Press "Enter" to skip to content

ListBox, WPF, GetHashCode e Strani comportamenti

La scorsa settimana, ero in ferie quindi mi sono dedicata a produrre qualcosa di nuovo per Dotnetwork, proseguendo con i tutorial dedicati a classi, librerie, applicazioni, servizi e database, visto che era arrivato il momento di iniziare a lavorare con i Dati, dopo aver costruito il necessario a manipolare e gestire un database, ho iniziato a studiare il metodo più semplice per costruire una classe Data Provider in grado di fornire le operazioni base di CRUD (CReate, UpDate, Delete) su una tabella database SQL Server evitando di dover chiamare in causa immediatamente Entity Framework, ma utilizzando POCO (Plain Old COde) e soprattutto poco codice davvero.

Ultimata la classe Data Provider e quanto necessario al suo utilizzo, ho iniziato a creare una User Interface per il nostro database Recipes, che abbiamo iniziato a vedere una cinquantina di post or sono, come esempio per la creazione di un database funzionante utilizzando solo T-SQL. Ho utilizzato quanto sviluppato fino ad ora nelle librerie base Dotnetwork ed ho creato un’ interfaccia di base, e il primo User Control per una tabella Sql Server.

MU_UI_01 

Il risultato è quello che vedete qui sopra, una specie di Tabbed MDI, con un TabItem chiudibile che ospita l’editor delle unità di misura. E’ solo una bozza,  ma almeno le basi le fa correttamente, o almeno così pensavo. Infatti, questo post non spiega come creare l’interfaccia che vedete nell’immagine invece è dedicato a lasciare memoria di un problema riscontrato e della sua soluzione, nonché aggiungere una lettura interessante al riguardo.

Il problema

Quando usando il tasto [New] creavo una nuova unità di misura, assegnandogli poi un ID, una Descrizione ed eventualmente i dati per la conversione ad altra unità di misura, la prima volta tutto funzionava, all’inserimento del secondo oggetto, la ListBox laterale impazziva, mostrando come selezionati entrambi i nuovi oggetti inseriti, e provando a spostarmi su di essa, ottenevo una “Duplicate Key exception” su qualcosa che non era all’interno del mio codice ma all’interno delle librerie collegate ad esso, ovvero le librerie dei controlli standard di WPF.

MU_UI_02

Guardando l’immagine qui sopra potreste dire, ti eri dimenticata di indicare che la listbox era a selezione singola. Sfortunatamente non è così, quella era la prima cosa controllata.

<ListBox Grid.Row="0" Grid.Column="0" 
     ItemsSource
="{Binding Path=MeasureUnits, Mode=OneWay}" 
     SelectedItem
="{Binding Path=SelectedMeasureUnit, Mode=TwoWay}"
     SelectionMode="Single" Margin="0"  
     IsSynchronizedWithCurrentItem
="True" >
<ListBox.ItemTemplate>
     <DataTemplate>
       <StackPanel>
         <WrapPanel>
            <TextBlock Text="{Binding Path=ID}" 
                Style
="{StaticResource idStyle}" />
           <TextBlock Text="{Binding Path=Description}" 
                Style
="{StaticResource desStyle}"/>
         </WrapPanel>
        <WrapPanel>
           <TextBlock Text="{Binding Path=DestinationUnitsInSource}"
               
Style="{StaticResource numStyle}" />
           <TextBlock Text="{Binding Path=SourceUnitsInDestination}" 
                Style
="{StaticResource numStyle}" />
          </WrapPanel>
       </StackPanel>
      </DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

Qui sopra lo XAML della mia listbox che contiene quanto necessario a fornire la selezione singola.

Dopo vari tentativi e dopo una consulenza con The Doctor, il mio collega Augusto che è molto più bravo di me con i motori di ricerca, abbiamo scoperto che il problema poteva essere legato all’ HashCode degli elementi della collection inserita in binding sulla ListBox.

Perché, una collezione che deriva da una List<T> che non ha nulla a che vedere con le Hash tables potesse avere un problema di Hash Code non mi risultava comprensibile, però, il fatto che l’errore di Duplicate Key che veniva indicato dal sistema e non dal mio codice mi riportava alla possibilità che la libreria dei controlli standard WPF usasse una Hash Table per la costruzione dei suoi oggetti destinati alla gestione della listbox, quindi la duplicate key fosse un problema dovuto al modo in cui noi calcolavamo l’ Hashcode degli oggetti della collection.

Pertanto come sempre, era colpa mia. (Ricordo sempre a chi legge che 99,9 periodico volte su 100 è colpa nostra.) Sono quindi andata a verificare l’entity contenuta nella collection e come implementa il suo GetHashCode.

public override intGetHashCode()
{
    return ID.GetHashCode();
}
 
public override bool Equals(object obj)
{
    MeasureUnit other = obj as MeasureUnit;
    if (other == null) return false;
    if (other.ID != this.ID) return false;
    return (true);
}

Nella mia entità MeasureUnit, ho effettuato l’override del metodo Equals, che viene utilizzato per non permettere di inserire due unità di misura con lo stesso codice (ID), se provate a fare un override di Equals, vi viene specificato che quando si effettua l’override di Equals, è opportuno effettuare l’override di GetHashCode, e inoltre, le linee guida dicono che usualmente, l’hashcode per una classe derivata deve essere l’hashcode del campo che rende univoco l’oggetto stesso.

Pertanto il mio codice è giusto e funziona perfettamente, in teoria, in pratica invece no. Infatti, c’è qualcosa che nella mia ignoranza (e per fortuna so di non sapere) ho trascurato.

In teoria, il mio GetHashCode è corretto, ma in pratica non funziona, perché la gestione dei controlli WPF utilizza una linea guida che io ignoravo, questa linea guida dice:

Lo HashCode di un oggetto, non deve essere modificato lungo tutta l’esistenza dell’oggetto stesso.

Per saperne di più vi consiglio di leggere questo articolo che spiega l’uso degli HashCode nella gestione delle collection: http://blogs.msdn.com/b/ericlippert/archive/2011/02/28/guidelines-and-rules-for-gethashcode.aspx,

Nel caso della mia classe, essendo lo HashCode dell’oggetto uguale all’ Hashcode del suo campo ID esso poteva cambiare durante la vita dell’oggetto quando io creavo le nuove Unità di misura, infatti, alla nuova unità di misura, la mia User Interface assegna un valore provvisorio “<new_nnnn>” che poi l’utente modifica. Il fatto che il valore di questa stringa venga modificato, modifica di conseguenza lo Hashcode dell’oggetto, questo manda in subbuglio le collection interne al controllo ListBox di WPF e da origine al comportamento anomalo e poi a questa Duplicate Key fantomatica interna al sistema e non collegata direttamente al mio codice.

Come correggere il problema?

public override int GetHashCode()
{
   return base.GetHashCode();
}

Modificando in questo modo la GetHashCode della classe, pur non rispondendo ad una linea guida, ne rispetto un altra più importante.

Anche se non sono certa che usare lo HashCode di un oggetto per identificarlo univocamente all’interno di un controllo sia perfettamente ortodosso, posso comunque capire come chi ha sviluppato i controlli ListBox, ListView generici, dovendo poter gestire in modo automatico qualsiasi lista, aveva bisogno di identificare gli oggetti inseriti, quindi non metto in dubbio che l’uso sia del tutto giustificato. Con questa ne ho imparata un altra. Un gradino in più verso la cima della conoscenza di .Net ma la cima è sempre lontana.