Press "Enter" to skip to content

Come Usare due diverse liste per pilotare un singolo dettaglio

Questo esempio è stato creato in risposta ad un thread sul forum Microsoft che riguardava Un problema sull’assegnare il focus su una maschera con due liste in realtà il problema non è il focus sulle Liste, ma un problema un po’ più “tricky” più complicato e legato alla gestione del databinding.

Questo esempio mostra come aggirare questo tipo di problema, ma poi mostro anche come approccerei in modo diverso l’interfaccia per evitare il problema, vediamo intanto cosa abbiamo creato e come funziona e poi discuteremo una possibile alternativa allo scenario.

mainwindow_01

Da quanto indicato da chi ha posto il quesito sul forum, la sua interfaccia per la messaggistica interna all’azienda ha una finestra con due liste, messaggi IN e messaggi OUT, al click di un messaggio, il suo contenuto viene visualizzato nella parte di dettaglio.  Il problema segnalato è che quando l’utente passa da una lista all’altra il messaggio selezionato non cambia sempre e quindi l’utente non comprende cosa sta guardando.

Vediamo come abbiamo implementato la parte XAML.

L’interfaccia

Ovviamente non ho visto l’interfaccia reale della persona che ha fatto la richiesta sul forum, ma mi è sembrato un problema interessante, abbastanza da creare un esempio WPF funzionante. Vediamo cosa ho inserito nella mia Main Window:

  • Grid – contiene tutto quello che abbiamo messo nella window, ha 3 righe, una contiene le liste (altezza 60%) una contiene i dettagli (altezza 40%) e la terza contiene i bottoni per le azioni utente (altezza Auto).
  • Grid – Contiene le 2 ListView, è formata da 2 righe x 2 colonne nella prima riga i titoli delle liste, nella seconda le liste
  • TextBlock– etichetta In Messages
  • TextBlock – etichetta Out Messages
  • ListView – In Messages, ItemsSource in binding all’elemento InMessages del view model, SelectedItem in binding all’elemento SelectedInMessage del view model. Due colonne definite, la prima con il flag che indica l’avvenuta lettura del messaggio, formattata con una CheckBox, la seconda, la stringa dell’oggetto del messaggio, i due campi di della lista sono in binding sui campi Read e Subject dell’Item della collection che fa da sorgente dati.
  • ListView – Out Messages, ItemsSource in binding all’elemento OutMessages del view model, SelectedItem in binding all’elemento SelectedOutMessage del view model. Due colonne definite, la prima con il flag che indica che il messaggio è stato spedito, anche in questo caso formattata con una CheckBox, la seconda la stringa dell’oggetto del messaggio, i due campi della lista sono in binding sui campi ReadSent e Subject della collection che fa da sorgente dati.
  • Grid – contiene i controlli dei dettagli, al suo interno 3 righe, le prime due con altezza Auto, la terza con altezza Fill.
  • TextBlock – Etichetta Selected Message
  • TextBox – in binding al campo Subject dell’oggetto SelectedMessage del view model.
  • TextBox – In binding al campo Body dell’oggetto SelectedMessage del view model.
  • StackPanel – contiene i bottoni di comando dell’applicazione
  • Button – Mark as Read permette di marcare un messaggio In come letto
  • Button – Send permette di spedire un messaggio Out
  • Button – New Out Message – Produce un nuovo messaggio di Out

Pure essendo un esempio, per riprodurre un applicazione funzionante ho creato un interfaccia complessa, che si occupa di visualizzare e modificare elementi di tipo Message. Non mi dilungherò oltre nello xaml, perché assumo che chi legge questo articolo conosca a sufficienza lo XAML da poterlo leggere senza problemi con le indicazioni qui sopra listate.

La classe Message

Volendo creare un esempio che per quanto in modo basico implementi MVVM per prima cosa ho creato la classe Message, che viene manipolata dalla mia interfaccia.

public enum MessageType
{
NotSet,
In,
Out
}

Per riconoscere e manipolare i nostri messaggi, ho creato un enumerazione che permette di riconoscere i messaggi ricevuti (In) rispetto a quelli inviati (Out).

public class Message : INotifyPropertyChanged
{



public const string FLD_Body = "Body";


public const string FLD_Read = "Read";


public const string FLD_Sent = "Sent";


public const string FLD_Subject = "Subject";


public const string FLD_Type = "Type";


private string mBody;


private bool? mRead;


private bool? mSent;


private string mSubject;


private MessageType mType;


public event PropertyChangedEventHandler PropertyChanged;


public Message()
{
Type = MessageType.NotSet;
}



public string Body
{
get
{
return mBody;
}
set
{
mBody = value;
OnPropertyChanged(FLD_Body);
}
}


public bool? Read
{
get
{
return mRead;
}
set
{
mRead = value;
OnPropertyChanged(FLD_Read);
}
}


public bool? Sent
{
get
{
return mSent;
}
set
{
mSent = value;
OnPropertyChanged(FLD_Sent);
}
}


public string Subject
{
get
{
return mSubject;
}
set
{
mSubject = value;
OnPropertyChanged(FLD_Subject);
}
}


public MessageType Type
{
get
{
return mType;
}
set
{
mType = value;
OnPropertyChanged(FLD_Type);
}
}


protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}


}

Per quanto si tratti di una classe molto semplice, un entity da utilizzare sulla UI di WPF ha bisogno di qualcosa di fondamentale, ovvero implementare l’Interfaccia INotifyPropertyChanged, se vogliamo che al variare del contenuto di una variabile l’interfaccia reagisca e mostri il cambiamento. Aggiungo, che non amando scrivere e riscrivere stringhe nei miei programmi, quando si tratta di Entity, i Nomi delle property preferisco dichiararli come costanti, in modo che se li uso in giro per il codice, non li sbaglierò creando errori molto difficili da riconoscere e debuggare. Spero che il codice non sia troppo complicato da capire, definisce una classe con 5 property:

  • Body – stringa, contiene il corpo del messaggio
  • Read – Booleano nullabile, per i messaggi In indica che il messaggio è stato letto, per i messaggi Out non viene usato
  • Sent – Booleano nullabile, per i messaggi Out indica che il messaggio è stato spedito, per i messaggi In non viene usato
  • Subject – stringa, contiene l’oggetto del messaggio
  • Type – del tipo enumerato MessageType prima definito, indica se il messaggio è In oppure Out.

La classe MainWindowModel

Questa classe è il ViewModel della MainWindow, creato per dare almeno qualche nozione in più di quelle basiche sull’implementazione di MVVM, si tratta anche in questo caso di qualcosa di davvero semplice, però abbiamo separato la gestione di tutte le attività sui dati e i dati forniti alla Window, dalla Window Stessa in una ‘parvenza’ di implementazione del pattern MVVM.

Cosa troveremo nella classe:

  • L’implementazione di INotifyPropertyChanged, che ci permetterà di sollevare l’evento PropertyChanged alla modifica di alcune delle property per fare in modo che la UI reagisca alle modifiche dei dati.
  • Le costanti con i nomi di tutte le property (come ho spiegato questo è un pattern che a me piace seguire perché scrivere a mano la stringa col nome di una property per lanciare un PropertyChanged porta alla possibilità di errori di digitazione e conseguenti malfunzionamenti difficilmente debuggabili.
  • Le variabili a supporto delle property (spiegheremo le property fra poco)
  • La property InMessages, una ObservableCollection generica di oggetti Message, che conterrà tutti i messaggi In.
  • La property IsNotWriteable, un boolean ricalcolato che ci permetterà di attivare e disattivare la possibilità di modificare i messaggi nel dettaglio dei dati permettendo di cambiarli solo se sono messaggi Out e non sono ancora spediti.
  • La property MarkAsReadIsEnabled, una property boolean calcolata che permetterà di attivare e disattivare il tasto Mark As Read solo se siamo su un messaggio In.
  • La property OutMessages, una ObservableCollection generica di oggetti Message, che conterrà tutti i messaggi Out.
  • La property SelectedInMessage , una property di tipo Message, che è in Binding al SelectedItem della Listview dei messaggi In, perché avere una variabile specifica per ogni lista? Perché usare una variabile singola sfortunatamente porta ad alcuni problemi.
  • La property SelectedMessage, anche questa di tipo Message, è la property le cui property (scusate il garbuglio) sono in binding con il dettaglio del messaggio correntemente selezionato, vedremo come sarà collegata alla precedente e a tutta una serie di altre property del ViewModel.
  • La property SelectedOutMessage, una property di tipo Message, che è in Binding al SelectedItem della ListView dei messaggi Out, anche in questo caso la variabile specifica è necessaria e vedremo perché.
  • La property a sola lettura SendIsEnabled, una property boolean calcolata che è vera solo se il SelectedMessage è di tipo Out e sarà usata per attivare e disattivare il tasto Send.
  • L’implementazione dei metodi che modificano i dati.

Vediamo in dettaglio il codice per le cose importanti.

public Message SelectedInMessage
{
get
{
return mSelectedInMessage;
}
set
{
mSelectedInMessage = value;
SelectedMessage = SelectedInMessage;
OnPropertyChanged(FLD_SelectedInMessage);
}
}

La property SelectedInMessage, potete notare come, in questo esempio, invece di creare delle dependency property nel ViewModel, ho semplicemente implementato l’interfaccia INotifyPropertyChanged sollevando l’evento con OnPropertyChanged ogni volta che una property viene aggiornata si tratta di una delle possibilità che abbiamo, ciascuno può utilizzare quella che gli è più consona.

Inoltre, quando la Property SelectedInMessage viene modificata, aggiorno con quella property anche il SelectedMessage vedremo perchè il doppio passaggio.

public Message SelectedOutMessage
{
get
{
return mSelectedOutMessage;
}
set
{
mSelectedOutMessage = value;
SelectedMessage = SelectedOutMessage;
OnPropertyChanged(FLD_SelectedOutMessage);
}
}

Anche la property SelectedOutMessage, aggiorna a sua volta SelectedMessage.

public Message SelectedMessage
{
get
{
return mSelectedMessage;
}
set
{
mSelectedMessage = value;
OnPropertyChanged(FLD_SelectedMessage);
OnPropertyChanged(FLD_IsNotWriteable);
OnPropertyChanged(FLD_MarkAsReadIsEnabled);
OnPropertyChanged(FLD_SendIsEnabled);
}
}

SelectedMessage, quando viene modificata oltre al property changed per se stessa, scatena anche l’evento per tutte le property calcolate, in modo tale che la UI aggiorni i valori in Binding ad esse collegati (Read Only o meno sulle Textbox e Attivo/Disattivo sui button.

internal void LoadSampleData()
{
Random rnd = new Random(DateTime.Now.Millisecond);
bool read = false;
for (int x = 0; x < 10; x++)
{
int i = rnd.Next(0, mSampleTexts.Length - 1);
Message msg = new Message();
msg.Subject = string.Format("{0} In, {1}", x, mSampleTexts[i].Substring(0, 40));
msg.Body = mSampleTexts[i];
msg.Read = read;
read = !read;
msg.Type = MessageType.In;
InMessages.Add(msg);
}
Thread.Sleep(100);

bool sent = true;
rnd = new Random(DateTime.Now.Millisecond);
for (int x = 0; x < 10; x++)
{
int i = rnd.Next(0, mSampleTexts.Length - 1);
Message msg = new Message();
msg.Subject = string.Format("{0} Out, {1}", x, mSampleTexts[i].Substring(0, 40));
msg.Body = mSampleTexts[i];
msg.Sent = sent;
sent = !sent;
msg.Type = MessageType.Out;
OutMessages.Add(msg);
}
}

Il metodo LoadSampleData, viene lanciato all’apertura della Window sull’evento Loaded e genera una decina di messaggi di ciascun tipo con valori casuali, lo sleep di qualche millisecondo è un trucco, serve perché il sistema è così veloce che altrimenti il seed del random è lo stesso.

internal void MarkAsRead(MainWindow mainWindow)
{
if (SelectedMessage.Type == MessageType.In)
{
SelectedMessage.Read = true;
//Here insert more code if you have to update the database of messages
}
}

Il metodo MarkAsRead che viene chiamato alla pressione del tasto Mark as read, aggiorna il messaggio corrente indicando che è stato letto quando è un messaggio ricevuto.

internal void NewOutMessage()
{
Message msg = new Message();
msg.Type = MessageType.Out;
msg.Subject = "<new message>";
msg.Sent = false;
OutMessages.Add(msg);
SelectedOutMessage = msg;
}

Il metodo NewOutMessage, chiamato dal tasto New Out Message che genera un nuovo messaggio da spedire, come lo fa:  crea un oggetto Message, mette un testo nell’oggetto per rendere semplice la modifica e lo aggiunge ai messaggi Out selezionandolo.

internal void SendMessage()
{
if (SelectedMessage.Type == MessageType.Out)
{
SelectedMessage.Sent = true;
OnPropertyChanged(FLD_IsNotWriteable);
//Here perform actions to send the message
}
}

Il metodo SendMessage, che spedisce un messaggio di output, ovviamente la sola cosa che facciamo è marcarne il flag Sent, ma possiamo implementare la logica che manda il messaggio ad esempio via HTTP oppure scrive semplicemente su un database in questo metodo, notate come una volta “spedito” forziamo l’aggiornamento della property IsNotWriteable perché la UI aggiorni le Textbox impostandole come ReadOnly per evitare che un messaggio spedito possa essere aggiornato.

La semplice implementazione del Binding e della classe ViewModel, fa si che la User interface così implementata  funzioni apparentemente bene.

mainwindow_02

Se facciamo Click su un messaggio IN lo mostra sul dettaglio e attiva Mark As Read

mainwindow_03

Click su un messaggio OUT lo mostra sul dettaglio e attiva Send.

Ma se proviamo a cliccare nuovamente sul messaggio IN già selezionato cosa accade:

mainwindow_04

Perché il dettaglio in questo caso non viene aggiornato?

Per un motivo molto semplice, perché la ListView non scatena il SelectedItemChanged e non modifica il SelectedInMessage e di conseguenza non modifica neppure il SelectedMessage e quindi il dettaglio non viene aggiornato.

Come risolvere? Il primo passo per lavorare correttamente è stato distinguere il SelectedItem delle 2 diverse Liste. In modo da poter acquisire il reale valore dell’oggetto selezionato di ogni lista. Se il SelectedItem di entrambe le liste fosse stato messo in Binding su SelectedMessage, non saremmo in grado di sapere quale messaggio selezionare quando cambia la Lista attiva.

Il secondo passo, sarà utilizzare un evento per controllare e forzare l’aggiornamento di SelectedMessage.

private void ListViewOut_GotFocus(object sender, RoutedEventArgs e)
{
ViewModel.SelectedMessage = ViewModel.SelectedOutMessage;
}



private void ListViewIn_GotFocus(object sender, RoutedEventArgs e)
{
ViewModel.SelectedMessage = ViewModel.SelectedInMessage;
}

Nel code behind della MainWindow, gestiamo l’evento GotFocus delle due liste, andando ad aggiornare il SelectedMessage con il SelectedInMessage quando la ListView a ricevere il focus è la ListView dei messaggi IN, con il SelectedOutMessage se la ListView selezionata è quella dei messaggi Out.

Quale è il problema primario per cui non ci basta MVVM, ma dobbiamo usare anche gli eventi? A mio avviso il problema è dovuto alla necessità di usare 2 Liste, non so quale sia il motivo per cui chi ha posto il quesito sul forum ha dovuto fare questo tipo di implementazione, magari i messaggi arrivano da fonti diverse o hanno diverso formato, non saprei, vediamo invece come implementerei io l’interfaccia, utilizzando altri mezzi messi a disposizione da WPF per permettere all’utente di avere un interfaccia pratica e a mio avviso più semplice.

mainwindow_05

Ho modificato la MainWindow aggiungendo un Button per generare una nuova finestra alternativa.

Ho generato le seguenti nuove classi:

  • MainWindowAlternate – la nuova interfaccia
  • MainWindowAlternateModel – Il nuovo ViewModel

mainwindowa_01

Come potete notare la MainWindowAlternate  è un po’ diversa, nel senso che è più semplice, utilizzando una sola lista per tutti i messaggi, è più colorata, perché ho utilizzato le funzioni di conversione che sono uno dei maggiori pregi di WPF per creare dei segnalini visuali che identifichino a colpo d’occhio il tipo di messaggio (IN/OUT) usando in questo caso due icone a forma di freccia, e utilizzando dei pallini colorati per indicare se il messaggio IN è stato letto o meno, e se il messaggio OUT è stato spedito o meno.

A che cosa mi porta questo approccio?

  • Ho ridotto il numero dei componenti a video e semplificato il ViewModel
  • Non ho bisogno di fare nulla sugli eventi della lista perché il Binding delle property fa tutto quello che serve.

Vediamo di dare un po’ di indicazioni oggettive. Prima di tutto i componenti della MainWindowAlternate in XAML:

  • Grid – contiene tutto quello che abbiamo messo nella window, ha 3 righe, una contiene le liste (altezza 60%) una contiene i dettagli (altezza 40%) e la terza contiene i bottoni per le azioni utente (altezza Auto).
  • Grid – Contiene la ListView, è formata da 2 righe x 1 colonna nella prima riga il titolo della lista, nella seconda la lista
  • TextBlock– etichetta In/Out Messages
  • ListView –  ItemsSource in binding all’elemento Messages del view model, SelectedItem in binding all’elemento SelectedMessage del view model. 4 colonne definite, la prima con il tipo di messaggio, la seconda con lo stato di lettura, la terza con lo stato di spedizione e la quarta con l’oggetto.
  • Grid – contiene i controlli dei dettagli, al suo interno 3 righe, le prime due con altezza Auto, la terza con altezza Fill.
  • TextBlock – Etichetta Selected Message
  • TextBox – in binding al campo Subject dell’oggetto SelectedMessage del view model.
  • TextBox – In binding al campo Body dell’oggetto SelectedMessage del view model.
  • StackPanel – contiene i bottoni di comando dell’applicazione
  • Button – Mark as Read permette di marcare un messaggio In come letto
  • Button – Send permette di spedire un messaggio Out
  • Button – New Out Message – Produce un nuovo messaggio di Out

Visto che abbiamo creato delle colonne con delle immagini che rappresentano dei dati enumerati o booleani, vediamo come si fa a definire la colonna nella ListView:

<GridViewColumn Header="Type" >
<GridViewColumn.CellTemplate>
<DataTemplate>
<Image
Margin="0"
MaxWidth="16"
MaxHeight="16"
Source="{Binding Type, Converter={StaticResource messageTypeToImageConverter}}"
ToolTip="{Binding Type, Converter={StaticResource messageTypeToStringConverter}}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>

Per creare una ListView multi colonna, bisogna generare la lista delle sue colonne, ogni colonna può essere semplicemente definita indicando il suo Header ed il DisplayMemberBinding relativo al campo da mostrare, oppure è possibile creare un template per la cella dove possiamo mettere quello che ci pare.

Nel nostro caso, abbiamo inserito un controllo Image, abbiamo indicato che 2 delle sue property sono in Binding con il campo Type dell’Item mostrato dalla ListView e, per ottenere l’immagine ed il suo tooltip con una descrizione intelligibile, abbiamo utilizzato due Converter. Ho mostrato una sola colonna, ma anche le altre due sono simili, cambiano solo i converter utilizzati.

I Converter sono delle classi che ci permettono di convertire un tipo di dato in uno completamente diverso, in questo caso un MessageType diviene una BitmapImage in un caso, una String in un altro.

    [ValueConversion(typeof(MessageType), typeof(BitmapImage))]
public class MessageTypeToImageConverter : IValueConverter
{


private static BitmapImage[] mImages;


static MessageTypeToImageConverter()
{
mImages = new BitmapImage[] {
//Image IN
new BitmapImage(new Uri("pack://application:,,,/Images/btn_032_985.png", UriKind.RelativeOrAbsolute))
//Image Out
,new BitmapImage(new Uri("pack://application:,,,/Images/btn_032_986.png", UriKind.RelativeOrAbsolute))
};
}


public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null)
return null;
BitmapImage result = null;
MessageType input = (MessageType)value;
switch (input)
{
case MessageType.In:
result = mImages[0]; //Image in
break;
case MessageType.Out:
result = mImages[1]; //Image out
break;
}
return result;
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value;
}



}

Questo è il Converter dell’immagine, per mia forma mentale, cerco di evitare di generare milioni di immagini quando le uso, pertanto nei converter con immagini, creo le immagini una sola volta in un costruttore statico e poi le restituisco quando richiesto, questo perché i converter vengono sempre chiamati milioni di volte.

[ValueConversion(typeof(MessageType), typeof(string))]
public class MessageTypeToStringConverter : IValueConverter
{


public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string result = "Not Set";
if (value != null)
{
MessageType input = (MessageType)value;
switch (input)
{
case MessageType.In:
result = "Received Message (IN)";
break;
case MessageType.Out:
result = "Sent Message (OUT)";
break;
}
}
return result;
}


public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
//If you don't need a convert back this does nothing
return value;
}

}

Il Converter per la stringa è molto più semplice, perché l’immutabilità delle stringhe fa in modo che una stringa sia comunque prodotta una sola volta.

Abbiamo diminuito i controlli ma implementato nuove classi, vero ma visualmente non è più bello e a mio avviso più facile da capire a colpo d’occhio? Se avessimo voluto abbellire anche l’interfaccia con le 2 liste avremmo comunque dovuto generare i converter.

Se andate a curiosare nella classe MainWindowAlternateModel, vedrete che ci sono meno property, e c’è meno codice dietro alla finestra.

L’esempio che potete scaricare al link qui sotto è completamente funzionante e contiene tutte le classi anche quelle che non abbiamo commentato per cercare di essere concisi.

Riepilogo

Cosa abbiamo visto in questo esempio di applicazione WPF:

  • Un uso basico di MVVM
  • L’uso del controllo ListView in modo non banale
  • L’uso degli eventi dei controlli per aiutarci quando MVVM non basta
  • L’uso dei converter per modificare visualmente dei dati ti tipo booleano ed enumerato.

Potete scaricare il progetto esempio dal link qui indicato:

Per qualsiasi domanda, osservazione, commento, approfondimento, o per segnalare un errore, potete usare il link alla form di contatto in cima alla pagina.