Parlando di M-V-VM ho notato che, appena appresi i concetti base, la prima domanda che scaturisce è: “Ok, ma come faccio a far comunicare tra loro i diversi ViewModels?”.
Vediamo di realizzare un semplice esempio basato sul M-V-VM Light toolkit che mostra una finestra con una lista di prodotti e un altra finestra contente i dettagli del prodotto selezionato,
La struttura dell’applicazione è molto semplice
- Alla finestra principale MainWindow è associato un MainViewModel
- Alla finestra di dettaglio EditDetailWindow è associato invece un EditDetailViewModel
- L’associazione avviene secondo questo post
Lo scambio di informazioni/comandi tra i vari viemodels è ottenuto usando una tecnica ormai diffusa tra le varie implementazioni di M-V-VM: Il mediator pattern.
Questo post non ha come obiettivo quello di descrivere il pattern, ma per brevità diciamo che è un sistema di scambio messaggi dove ogni partecipante decide quali sono i messaggi che desidera ricevere, nel nostro caso il MainViewModel recupererà l’item selezionato e invierà un messaggio EditDetailMessage allo scambiatore di messaggi, che nel M-V-VM toolkit è rappresentato dalla classe Messenger (in Prism, ad esempio, si chiama EventAggregator).
Vediamo com’è fatto il MainViewModel
1: public class MainViewModel : ViewModelBase
2: {
3: public MainViewModel()
4: {
5: //Add some dummy products
6: this.Items = new ObservableCollection<Product>()
7: {
8: new Product(){ Description="Product #1", Quantity=12},
9: new Product(){ Description="Product #2", Quantity=42},
10: new Product(){ Description="Product #3", Quantity=7}
11: };
12:
13: this.ButtonCommand = new RelayCommand(() =>
14: {
15: //Get selected item
16: ICollectionView cvs=CollectionViewSource.GetDefaultView(this.Items);
17: Product selectedProduct = (Product)cvs.CurrentItem;
18: //Broadcast message
19: var editDetailsMessage = new EditDetailMessage(this,selectedProduct);
20: Messenger.Default.Broadcast(editDetailsMessage);
21: });
22: }
23:
24: public RelayCommand ButtonCommand
25: {
26: get;
27: private set;
28: }
29:
30: public ObservableCollection<Product> Items
31: {
32: get;
33: private set;
34: }
35: }
A questo punto sorge spontaneo chiedersi: “Chi lo processa questo messaggio?” e qui andrebbe aperta una lunga parentesi relativamente al fatto che in un applicazione basata su Model-View-ViewModel sia tabù aggiungere codice nel code-behind.
Personalmente non sono d’accordo e non trovo nulla di sbagliato nell’inserire nel code-behind del codice a patto che questo abbia esclusivamente a che fare operazioni relative alla view (come me la pensano Davide, Glenn Block e molti altri…)
Vediamo come è fatto il code-behind di MainWindow:
1: public partial class MainWindow : Window, IMessageRecipient
2: {
3: public MainWindow()
4: {
5: //Registers code-behind as subscriber of EditDetailMessage
6: Messenger.Default.Register(this, typeof(EditDetailMessage));
7: this.InitializeComponent();
8: }
9:
10: public void ReceiveMessage(MessageBase message)
11: {
12: EditDetailMessage edm = message as EditDetailMessage;
13: if (edm != null)
14: {
15: //Creates detail window
16: EditDetalWindow edw = new EditDetalWindow();
17: //Post ShowDetailWindow message
18: ShowDetailMessage sdm = new ShowDetailMessage(this, edm.Content);
19: Messenger.Default.Broadcast(sdm);
20: //Show Detail Window
21: edw.Show();
22: }
23: }
24: }
Come vedete nel costruttore viene ‘sottoscritto’ il messaggio EditDetailMessage, mentre in ReceiveMessage (definito nell’interfaccia IMessageRecipient che MainWindow implementa) questo viene elaborato, creata la finestra di dettaglio (e questa è a tutti gli effetti un operazione che ha a che fare con la View) e postato un nuovo messaggio ShowDetailMessage.
Il destinatario di ShowDetailMessage è ovviamente il nostro EditDetailViewModel:
1: public class EditDetailViewModel : ViewModelBase, IMessageRecipient
2: {
3: private Product product=null;
4:
5: public EditDetailViewModel()
6: {
7: //Subscribes ShowDetail Message
8: Messenger.Default.Register(this, typeof(ShowDetailMessage));
9: }
10:
11: public Product Product
12: {
13: get { return this.product; }
14: set
15: {
16: this.product = value;
17: base.RaisePropertyChanged("Product");
18: }
19: }
20:
21: public void ReceiveMessage(MessageBase message)
22: {
23: ShowDetailMessage sdm = message as ShowDetailMessage;
24: if (sdm != null)
25: {
26: this.Product = sdm.Content;
27: }
28: }
29: }
il quale aggiornerà la proprietà Product esposta dal ViewModel la quale è in binding con gli elementi che compongono la UI di EditDetailWindow.
Non entro nel dettagli di come vengono realizzati i messaggi in quanto argomento specifico del toolkit che ho utilizzato (ad esempio in Prism si ottiene derivando dalla classe CompositePresentationEvent) ma spero che il concetto di “messaging” sul quale si basa M-V-VM sia stato sufficientement chiaro.
Quindi di fronte alla classica domanda: “Ma come faccio a visualizzare una message box con M-V-VM?” la risposta è: “Far generare al viewmodel un messaggio e processarlo nel code-behind”
Nota: Mockando IMessageRecipeint si può ovviamente sottoporre a unit testing l’intero ViewModel.
Code-behind is not evil!