MVVM - Gerarchie e navigazione
Quando si creano applicazioni MVVM, in genere si scompongono schermate complesse di informazioni in un set di visualizzazioni padre e figlio, in cui le visualizzazioni figlio sono contenute all'interno delle visualizzazioni padre in pannelli o controlli contenitore e formano una gerarchia di utilizzo.
Dopo aver scomposto le viste complesse, non significa che ogni parte del contenuto figlio che si separa nel proprio file XAML deve necessariamente essere una vista MVVM.
La porzione di contenuto fornisce solo la struttura per visualizzare qualcosa sullo schermo e non supporta alcun input o manipolazione da parte dell'utente per quel contenuto.
Potrebbe non essere necessario un ViewModel separato, ma potrebbe essere solo un blocco XAML che esegue il rendering in base alle proprietà esposte dal ViewModel padre.
Infine, se hai una gerarchia di Views e ViewModels, il ViewModel padre può diventare un hub per le comunicazioni in modo che ogni ViewModel figlio possa rimanere disaccoppiato dagli altri ViewModels figlio e dal loro genitore il più possibile.
Diamo un'occhiata a un esempio in cui definiremo una semplice gerarchia tra diverse viste. Crea un nuovo progetto di applicazione WPFMVVMHierarchiesDemo
Step 1 - Aggiungi le tre cartelle (Model, ViewModel e Views) al tuo progetto.
Step 2 - Aggiungere le classi Customer e Order nella cartella Model, CustomerListView e OrderView nella cartella Views e CustomerListViewModel e OrderViewModel nella cartella ViewModel come mostrato nell'immagine seguente.
Step 3- Aggiungi blocchi di testo sia in CustomerListView che in OrderView. Ecco il file CustomerListView.xaml.
<UserControl x:Class="MVVMHierarchiesDemo.Views.CustomerListView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMHierarchiesDemo.Views"
mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<Grid>
<TextBlock Text = "Customer List View"/>
</Grid>
</UserControl>
Di seguito è riportato il file OrderView.xaml.
<UserControl x:Class = "MVVMHierarchiesDemo.Views.OrderView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc ="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d ="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMHierarchiesDemo.Views" mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<Grid>
<TextBlock Text = "Order View"/>
</Grid>
</UserControl>
Ora abbiamo bisogno di qualcosa per ospitare queste viste, e un buon posto per questo nella nostra MainWindow perché è una semplice applicazione. Abbiamo bisogno di un controllo contenitore che possiamo posizionare le nostre visualizzazioni e cambiarle in modo di navigazione. A tale scopo, dobbiamo aggiungere ContentControl nel nostro file MainWindow.xaml e utilizzeremo la sua proprietà content e lo assoceremo a un riferimento ViewModel.
Definisci ora i modelli di dati per ogni vista in un dizionario risorse. Di seguito è riportato il file MainWindow.xaml. Nota come ogni modello di dati associa un tipo di dati (il tipo ViewModel) a una vista corrispondente.
<Window x:Class = "MVVMHierarchiesDemo.MainWindow"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local = "clr-namespace:MVVMHierarchiesDemo"
xmlns:views = "clr-namespace:MVVMHierarchiesDemo.Views"
xmlns:viewModels = "clr-namespace:MVVMHierarchiesDemo.ViewModel"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType = "{x:Type viewModels:CustomerListViewModel}">
<views:CustomerListView/>
</DataTemplate>
<DataTemplate DataType = "{x:Type viewModels:OrderViewModel}">
<views:OrderView/>
</DataTemplate>
</Window.Resources>
<Grid>
<ContentControl Content = "{Binding CurrentView}"/>
</Grid>
</Window>
Ogni volta che il modello di visualizzazione corrente è impostato su un'istanza di CustomerListViewModel, eseguirà il rendering di CustomerListView con ViewModel collegato. È un ViewModel di ordine, renderà OrderView e così via.
Ora abbiamo bisogno di un ViewModel che abbia una proprietà CurrentViewModel e un po 'di logica e comando per poter cambiare il riferimento corrente di ViewModel all'interno della proprietà.
Creiamo un ViewModel per questa MainWindow chiamato MainWindowViewModel. Possiamo semplicemente creare un'istanza del nostro ViewModel da XAML e usarla per impostare la proprietà DataContext della finestra. Per questo, dobbiamo creare una classe base per incapsulare l'implementazione di INotifyPropertyChanged per i nostri ViewModels.
L'idea principale alla base di questa classe è incapsulare l'implementazione INotifyPropertyChanged e fornire metodi di supporto alla classe derivata in modo che possano facilmente attivare le notifiche appropriate. Di seguito è riportata l'implementazione della classe BindableBase.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo {
class BindableBase : INotifyPropertyChanged {
protected virtual void SetProperty<T>(ref T member, T val,
[CallerMemberName] string propertyName = null) {
if (object.Equals(member, val)) return;
member = val;
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnPropertyChanged(string propertyName) {
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
}
}
Ora è il momento di iniziare effettivamente a cambiare visualizzazione utilizzando la nostra proprietà CurrentViewModel. Abbiamo solo bisogno di un modo per guidare l'impostazione di questa proprietà. E faremo in modo che l'utente finale possa comandare andando all'elenco dei clienti o alla visualizzazione degli ordini. Per prima cosa aggiungi una nuova classe nel tuo progetto che implementerà l'interfaccia ICommand. Di seguito è riportata l'implementazione dell'interfaccia ICommand.
using System;
using System.Windows.Input;
namespace MVVMHierarchiesDemo {
public class MyICommand<T> : ICommand {
Action<T> _TargetExecuteMethod;
Func<T, bool> _TargetCanExecuteMethod;
public MyICommand(Action<T> executeMethod) {
_TargetExecuteMethod = executeMethod;
}
public MyICommand(Action<T> executeMethod, Func<T, bool> canExecuteMethod) {
_TargetExecuteMethod = executeMethod;
_TargetCanExecuteMethod = canExecuteMethod;
}
public void RaiseCanExecuteChanged() {
CanExecuteChanged(this, EventArgs.Empty);
}
#region ICommand Members
bool ICommand.CanExecute(object parameter) {
if (_TargetCanExecuteMethod != null) {
T tparm = (T)parameter;
return _TargetCanExecuteMethod(tparm);
}
if (_TargetExecuteMethod != null) {
return true;
}
return false;
}
// Beware - should use weak references if command instance lifetime is
longer than lifetime of UI objects that get hooked up to command
// Prism commands solve this in their implementation
public event EventHandler CanExecuteChanged = delegate { };
void ICommand.Execute(object parameter) {
if (_TargetExecuteMethod != null) {
_TargetExecuteMethod((T)parameter);
}
}
#endregion
}
}
Ora abbiamo bisogno di impostare una navigazione di primo livello per questi a ViewModels e la logica per quel passaggio dovrebbe appartenere a MainWindowViewModel. Per questo utilizzeremo un metodo chiamato su navigate che accetta una destinazione stringa e restituisce la proprietà CurrentViewModel.
private void OnNav(string destination) {
switch (destination) {
case "orders":
CurrentViewModel = orderViewModelModel;
break;
case "customers":
default:
CurrentViewModel = custListViewModel;
break;
}
}
Per navigare tra queste diverse viste, dobbiamo aggiungere due pulsanti nel nostro file MainWindow.xaml. Di seguito è riportata l'implementazione completa del file XAML.
<Window x:Class = "MVVMHierarchiesDemo.MainWindow"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local = "clr-namespace:MVVMHierarchiesDemo"
xmlns:views = "clr-namespace:MVVMHierarchiesDemo.Views"
xmlns:viewModels = "clr-namespace:MVVMHierarchiesDemo.ViewModel"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType = "{x:Type viewModels:CustomerListViewModel}">
<views:CustomerListView/>
</DataTemplate>
<DataTemplate DataType = "{x:Type viewModels:OrderViewModel}">
<views:OrderView/>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height = "Auto" />
<RowDefinition Height = "*" />
</Grid.RowDefinitions>
<Grid x:Name = "NavBar">
<Grid.ColumnDefinitions>
<ColumnDefinition Width = "*" />
<ColumnDefinition Width = "*" />
<ColumnDefinition Width = "*" />
</Grid.ColumnDefinitions>
<Button Content = "Customers"
Command = "{Binding NavCommand}"
CommandParameter = "customers"
Grid.Column = "0" />
<Button Content = "Order"
Command = "{Binding NavCommand}"
CommandParameter = "orders"
Grid.Column = "2" />
</Grid>
<Grid x:Name = "MainContent" Grid.Row = "1">
<ContentControl Content = "{Binding CurrentViewModel}" />
</Grid>
</Grid>
</Window>
Di seguito è riportata l'implementazione completa di MainWindowViewModel.
using MVVMHierarchiesDemo.ViewModel;
using MVVMHierarchiesDemo.Views;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo {
class MainWindowViewModel : BindableBase {
public MainWindowViewModel() {
NavCommand = new MyICommand<string>(OnNav);
}
private CustomerListViewModel custListViewModel = new CustomerListViewModel();
private OrderViewModel orderViewModelModel = new OrderViewModel();
private BindableBase _CurrentViewModel;
public BindableBase CurrentViewModel {
get {return _CurrentViewModel;}
set {SetProperty(ref _CurrentViewModel, value);}
}
public MyICommand<string> NavCommand { get; private set; }
private void OnNav(string destination) {
switch (destination) {
case "orders":
CurrentViewModel = orderViewModelModel;
break;
case "customers":
default:
CurrentViewModel = custListViewModel;
break;
}
}
}
}
Deriva tutti i tuoi ViewModels dalla classe BindableBase. Quando il codice sopra viene compilato ed eseguito, vedrai il seguente output.
Come puoi vedere, abbiamo aggiunto solo due pulsanti e un CurrentViewModel sulla nostra MainWindow. Se fai clic su qualsiasi pulsante, passerà a quella vista particolare. Facciamo clic sul pulsante Clienti e vedrai che viene visualizzato CustomerListView.
Ti consigliamo di eseguire l'esempio precedente in modo graduale per una migliore comprensione.