MVVM - Guida rapida
Il modo ben ordinato e forse il più riutilizzabile per organizzare il codice è usare il pattern "MVVM". IlModel, View, ViewModel (MVVM pattern) significa guidarti su come organizzare e strutturare il tuo codice per scrivere applicazioni manutenibili, testabili ed estensibili.
Model - Contiene semplicemente i dati e non ha nulla a che fare con la logica aziendale.
ViewModel - Funge da collegamento / connessione tra il modello e la vista e rende le cose belle.
View - Contiene semplicemente i dati formattati e sostanzialmente delega tutto al Modello.
Presentazione separata
Per evitare i problemi causati dall'inserimento della logica dell'applicazione in code-behind o XAML, è preferibile utilizzare una tecnica nota come presentazione separata. Stiamo cercando di evitarlo, dove avremo XAML e code-behind con il minimo richiesto per lavorare direttamente con gli oggetti dell'interfaccia utente. Le classi dell'interfaccia utente contengono anche codice per comportamenti di interazione complessi, logica dell'applicazione e tutto il resto, come mostrato nella figura seguente sul lato sinistro.
Con la presentazione separata, la classe dell'interfaccia utente è molto più semplice. Ovviamente ha il codice XAML, ma il codice alla base fa il meno possibile.
La logica dell'applicazione appartiene a una classe separata, spesso denominata modello.
Tuttavia, questa non è l'intera storia. Se ti fermi qui, è probabile che ripeta un errore molto comune che ti porterà lungo il percorso della follia del data binding.
Molti sviluppatori tentano di utilizzare l'associazione dati per connettere elementi in XAML direttamente alle proprietà nel modello.
A volte questo può andare bene, ma spesso non lo è. Il problema è che il modello è interamente interessato a ciò che fa l'applicazione e non a come l'utente interagisce con l'applicazione.
Il modo in cui presenti i dati è spesso leggermente diverso da come sono strutturati internamente.
Inoltre, la maggior parte delle interfacce utente ha uno stato che non appartiene al modello dell'applicazione.
Ad esempio, se la tua interfaccia utente utilizza un drag and drop, qualcosa deve tenere traccia di cose come dove si trova l'elemento trascinato in questo momento, come dovrebbe cambiare il suo aspetto mentre si sposta su possibili obiettivi di rilascio e come potrebbero anche questi obiettivi di rilascio cambia quando l'elemento viene trascinato su di essi.
Questo tipo di stato può diventare sorprendentemente complesso e deve essere testato a fondo.
In pratica, normalmente si desidera che un'altra classe si trovi tra l'interfaccia utente e il modello. Questo ha due ruoli importanti.
Innanzitutto, adatta il modello dell'applicazione per una particolare visualizzazione dell'interfaccia utente.
In secondo luogo, è dove vive ogni logica di interazione non banale e, con questo, intendo il codice richiesto per far sì che la tua interfaccia utente si comporti nel modo desiderato.
Il pattern MVVM è in definitiva la struttura moderna del pattern MVC, quindi l'obiettivo principale è sempre lo stesso per fornire una chiara separazione tra logica di dominio e livello di presentazione. Ecco alcuni dei vantaggi e degli svantaggi del pattern MVVM.
Il vantaggio principale è consentire una vera separazione tra la vista e il modello oltre a raggiungere la separazione e l'efficienza che si guadagna da averla. Ciò che significa in termini reali è che quando il tuo modello deve cambiare, può essere cambiato facilmente senza che la vista ne abbia bisogno e viceversa.
Ci sono tre aspetti chiave importanti che derivano dall'applicazione di MVVM che sono i seguenti.
Manutenibilità
Una netta separazione di diversi tipi di codice dovrebbe rendere più facile entrare in una o più di quelle parti più granulari e mirate e apportare modifiche senza preoccuparsi.
Ciò significa che puoi rimanere agile e continuare a passare rapidamente alle nuove versioni.
Testabilità
Con MVVM ogni pezzo di codice è più granulare e se viene implementato correttamente le tue dipendenze esterne e interne sono in parti di codice separate dalle parti con la logica di base che desideri testare.
Ciò rende molto più semplice scrivere unit test rispetto a una logica di base.
Assicurati che funzioni correttamente quando viene scritto e continui a funzionare anche quando le cose cambiano durante la manutenzione.
Estensibilità
A volte si sovrappone alla manutenibilità, a causa dei confini di separazione netti e dei pezzi di codice più granulari.
Hai maggiori possibilità di rendere una qualsiasi di queste parti più riutilizzabile.
Ha anche la capacità di sostituire o aggiungere nuovi pezzi di codice che fanno cose simili nei posti giusti nell'architettura.
Lo scopo ovvio del pattern MVVM è l'astrazione della vista che riduce la quantità di logica aziendale nel code-behind. Tuttavia, di seguito sono riportati alcuni altri vantaggi concreti:
- ViewModel è più facile da testare rispetto al codice code-behind o basato su eventi.
- Puoi testarlo senza scomode automazioni e interazioni dell'interfaccia utente.
- Il livello di presentazione e la logica sono vagamente accoppiati.
Svantaggi
- Alcune persone pensano che per semplici UI, MVVM possa essere eccessivo.
- Allo stesso modo, nei casi più grandi, può essere difficile progettare ViewModel.
- Il debug sarebbe un po 'difficile quando abbiamo associazioni di dati complesse.
Il modello MVVM è costituito da tre parti: Model, View e ViewModel. La maggior parte degli sviluppatori all'inizio è poco confusa su cosa dovrebbe o non dovrebbe contenere un Model, View e ViewModel e quali sono le responsabilità di ciascuna parte.
In questo capitolo impareremo le responsabilità di ogni parte del pattern MVVM in modo che possiate capire chiaramente che tipo di codice va e dove. MVVM è davvero un'architettura a più livelli per il lato client, come mostrato nella figura seguente.
Il livello di presentazione è composto dalle viste.
Il livello logico sono i modelli di visualizzazione.
Il livello di presentazione è la combinazione degli oggetti del modello.
I servizi client che li producono e li mantengono hanno diretto l'accesso in un'applicazione a due livelli o tramite chiamate di servizio e quindi all'applicazione.
I servizi client non fanno ufficialmente parte del modello MVVM ma vengono spesso utilizzati con MVVM per ottenere ulteriori separazioni ed evitare codici duplicati.
Responsabilità del modello
In generale, il modello è il più semplice da capire. È il modello di dati lato client che supporta le visualizzazioni nell'applicazione.
È composto da oggetti con proprietà e alcune variabili per contenere dati in memoria.
Alcune di queste proprietà possono fare riferimento ad altri oggetti del modello e creare il grafico degli oggetti che nel suo insieme sono gli oggetti del modello.
Gli oggetti del modello dovrebbero generare notifiche di modifica delle proprietà che in WPF significa data binding.
L'ultima responsabilità è la convalida che è facoltativa, ma è possibile incorporare le informazioni di convalida sugli oggetti del modello utilizzando le funzionalità di convalida dell'associazione dati WPF tramite interfacce come INotifyDataErrorInfo / IDataErrorInfo
Visualizza responsabilità
Lo scopo principale e le responsabilità delle visualizzazioni è definire la struttura di ciò che l'utente vede sullo schermo. La struttura può contenere parti statiche e dinamiche.
Le parti statiche sono la gerarchia XAML che definisce i controlli e il layout dei controlli di cui è composta una visualizzazione.
La parte dinamica è come le animazioni o le modifiche di stato definite come parte della vista.
L'obiettivo principale di MVVM è che non ci dovrebbe essere alcun codice nella visualizzazione.
È impossibile che non ci sia codice in vista. In vista è almeno necessario il costruttore e una chiamata per inizializzare il componente.
L'idea è che il codice logico di gestione degli eventi, di azione e di manipolazione dei dati non dovrebbe essere nel codice sottostante in View.
Esistono anche altri tipi di codice che devono essere inseriti nel codice dietro qualsiasi codice necessario per avere un riferimento all'elemento dell'interfaccia utente è intrinsecamente codice di visualizzazione.
Responsabilità di ViewModel
ViewModel è il punto principale dell'applicazione MVVM. La responsabilità principale del ViewModel è fornire i dati alla vista, in modo che la vista possa inserire quei dati sullo schermo.
Consente inoltre all'utente di interagire con i dati e modificare i dati.
L'altra responsabilità chiave di un ViewModel è incapsulare la logica di interazione per una vista, ma non significa che tutta la logica dell'applicazione debba andare in ViewModel.
Dovrebbe essere in grado di gestire la sequenza appropriata delle chiamate per fare in modo che la cosa giusta avvenga in base all'utente o a eventuali modifiche alla vista.
ViewModel dovrebbe anche gestire qualsiasi logica di navigazione come decidere quando è il momento di passare a una visualizzazione diversa.
In questo capitolo impareremo come usare i pattern MVVM per una semplice schermata di input e l'applicazione WPF a cui potresti già essere abituato.
Diamo un'occhiata a un semplice esempio in cui useremo l'approccio MVVM.
Step 1 - Crea un nuovo progetto di applicazione WPF MVVMDemo.
Step 2 - Aggiungi le tre cartelle (Model, ViewModel e Views) al tuo progetto.
Step 3 - Aggiungi una classe StudentModel nella cartella Model e incolla il codice seguente in quella classe
using System.ComponentModel;
namespace MVVMDemo.Model {
public class StudentModel {}
public class Student : INotifyPropertyChanged {
private string firstName;
private string lastName;
public string FirstName {
get {
return firstName;
}
set {
if (firstName != value) {
firstName = value;
RaisePropertyChanged("FirstName");
RaisePropertyChanged("FullName");
}
}
}
public string LastName {
get {return lastName; }
set {
if (lastName != value) {
lastName = value;
RaisePropertyChanged("LastName");
RaisePropertyChanged("FullName");
}
}
}
public string FullName {
get {
return firstName + " " + lastName;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
}
Step 4 - Aggiungi un'altra classe StudentViewModel nella cartella ViewModel e incolla il codice seguente.
using MVVMDemo.Model;
using System.Collections.ObjectModel;
namespace MVVMDemo.ViewModel {
public class StudentViewModel {
public ObservableCollection<Student> Students {
get;
set;
}
public void LoadStudents() {
ObservableCollection<Student> students = new ObservableCollection<Student>();
students.Add(new Student { FirstName = "Mark", LastName = "Allain" });
students.Add(new Student { FirstName = "Allen", LastName = "Brown" });
students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" });
Students = students;
}
}
}
Step 5 - Aggiungi un nuovo controllo utente (WPF) facendo clic con il pulsante destro del mouse sulla cartella Visualizzazioni e seleziona Aggiungi> Nuovo elemento ...
Step 6- Fare clic sul pulsante Aggiungi. Ora vedrai il file XAML. Aggiungi il codice seguente nel file StudentView.xaml che contiene diversi elementi dell'interfaccia utente.
<UserControl x:Class = "MVVMDemo.Views.StudentView"
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:MVVMDemo.Views"
mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<Grid>
<StackPanel HorizontalAlignment = "Left">
<ItemsControl ItemsSource = "{Binding Path = Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
Step 7 - Ora aggiungi StudentView nel tuo file MainPage.xaml usando il codice seguente.
<Window x:Class = "MVVMDemo.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:MVVMDemo"
xmlns:views = "clr-namespace:MVVMDemo.Views"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Grid>
<views:StudentView x:Name = "StudentViewControl" Loaded = "StudentViewControl_Loaded"/>
</Grid>
</Window>
Step 8 - Ecco l'implementazione per l'evento Loaded nel file MainPage.xaml.cs, che aggiornerà la vista dal ViewModel.
using System.Windows;
namespace MVVMDemo {
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
}
private void StudentViewControl_Loaded(object sender, RoutedEventArgs e) {
MVVMDemo.ViewModel.StudentViewModel studentViewModelObject =
new MVVMDemo.ViewModel.StudentViewModel();
studentViewModelObject.LoadStudents();
StudentViewControl.DataContext = studentViewModelObject;
}
}
}
Step 9 - Quando il codice sopra è stato compilato ed eseguito, riceverai il seguente output nella finestra principale.
Ti consigliamo di eseguire l'esempio precedente in modo graduale per una migliore comprensione.
In questo capitolo, tratteremo diversi modi in cui puoi collegare le tue visualizzazioni a ViewModel. Per prima cosa, diamo uno sguardo alla prima costruzione di View in cui possiamo dichiararla in XAML. Come abbiamo visto nell'esempio nell'ultimo capitolo in cui abbiamo collegato una vista dalla finestra principale. Ora vedremo altri modi per collegare le visualizzazioni.
Useremo lo stesso esempio anche in questo capitolo. Di seguito è riportata la stessa implementazione della classe Model.
using System.ComponentModel;
namespace MVVMDemo.Model {
public class StudentModel {}
public class Student : INotifyPropertyChanged {
private string firstName;
private string lastName;
public string FirstName {
get { return firstName; }
set {
if (firstName != value) {
firstName = value;
RaisePropertyChanged("FirstName");
RaisePropertyChanged("FullName");
}
}
}
public string LastName {
get { return lastName; }
set {
if (lastName != value) {
lastName = value;
RaisePropertyChanged("LastName");
RaisePropertyChanged("FullName");
}
}
}
public string FullName {
get {
return firstName + " " + lastName;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
}
Ecco l'implementazione della classe ViewModel. Questa volta il metodo LoadStudents viene chiamato nel costruttore predefinito.
using MVVMDemo.Model;
using System.Collections.ObjectModel;
namespace MVVMDemo.ViewModel{
public class StudentViewModel {
public StudentViewModel() {
LoadStudents();
}
public ObservableCollection<Student> Students {
get;
set;
}
public void LoadStudents() {
ObservableCollection<Student> students = new ObservableCollection<Student>();
students.Add(new Student { FirstName = "Mark", LastName = "Allain" });
students.Add(new Student { FirstName = "Allen", LastName = "Brown" });
students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" });
Students = students;
}
}
}
Indipendentemente dal fatto che la vista sia una finestra, un controllo utente o una pagina, il parser generalmente funziona dall'alto in basso e da sinistra a destra. Chiama il costruttore predefinito per ogni elemento non appena lo incontra. Esistono due modi per costruire una vista. Puoi usarne uno qualsiasi.
- Visualizza la prima costruzione in XAML
- Visualizza la prima costruzione in Code-behind
Visualizza la prima costruzione in XAML
Un modo consiste nell'aggiungere semplicemente il tuo ViewModel come elemento nidificato nel setter per la proprietà DataContext come mostrato nel codice seguente.
<UserControl.DataContext>
<viewModel:StudentViewModel/>
</UserControl.DataContext>
Ecco il file XAML di visualizzazione completo.
<UserControl x:Class="MVVMDemo.Views.StudentView"
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:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<UserControl.DataContext>
<viewModel:StudentViewModel/>
</UserControl.DataContext>
<Grid>
<StackPanel HorizontalAlignment = "Left">
<ItemsControl ItemsSource = "{Binding Path = Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
Visualizza la prima costruzione in Code-behind
Un altro modo è che puoi ottenere la prima costruzione della vista semplicemente costruendo tu stesso il modello della vista nel codice dietro della tua vista impostando la proprietà DataContext lì con l'istanza.
In genere, la proprietà DataContext viene impostata nel metodo di visualizzazione del costruttore, ma è anche possibile rinviare la costruzione fino a quando non viene attivato l'evento Load della visualizzazione.
using System.Windows.Controls;
namespace MVVMDemo.Views {
/// <summary>
/// Interaction logic for StudentView.xaml
/// </summary>
public partial class StudentView : UserControl {
public StudentView() {
InitializeComponent();
this.DataContext = new MVVMDemo.ViewModel.StudentViewModel();
}
}
}
Uno dei motivi per costruire il modello di visualizzazione in Code-behind anziché in XAML è che il costruttore del modello di visualizzazione accetta parametri, ma l'analisi XAML può costruire elementi solo se definito nel costruttore predefinito.
In questo caso, il file XAML di View apparirà come mostrato nel codice seguente.
<UserControl x:Class = "MVVMDemo.Views.StudentView"
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:MVVMDemo.Views"
mc:Ignorable = "d"
d:DesignHeight = "300"
d:DesignWidth = "300">
<Grid>
<StackPanel HorizontalAlignment = "Left">
<ItemsControl ItemsSource = "{Binding Path = Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal"<
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
È possibile dichiarare questa visualizzazione nella MainWindow come mostrato nel file MainWindow.XAML.
<Window x:Class = "MVVMDemo.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:MVVMDemo"
xmlns:views = "clr-namespace:MVVMDemo.Views"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Grid>
<views:StudentView x:Name = "StudentViewControl"/>
</Grid>
</Window>
Quando il codice sopra è stato compilato ed eseguito, vedrai il seguente output nella finestra principale.
Ti consigliamo di eseguire l'esempio precedente in modo graduale per una migliore comprensione.
In questo capitolo vedremo come collegare ViewModel. È una continuazione dell'ultimo capitolo in cui abbiamo discusso la prima costruzione della Vista. Ora, la forma successiva della prima costruzione è ameta-pattern che è noto come ViewModelLocator. È uno pseudo pattern ed è sovrapposto al pattern MVVM.
In MVVM ogni vista deve essere collegata al suo ViewModel.
ViewModelLocator è un approccio semplice per centralizzare il codice e disaccoppiare maggiormente la visualizzazione.
Significa che non deve conoscere esplicitamente il tipo di ViewModel e come costruirlo.
Esistono diversi approcci per utilizzare ViewModelLocator, ma qui usiamo il più simile a quello che fa parte del framework PRISM.
ViewModelLocator fornisce un modo standard, coerente, dichiarativo e vagamente accoppiato per eseguire la prima costruzione della vista che automatizza il processo di collegamento di ViewModel alla vista. La figura seguente rappresenta il processo di alto livello di ViewModelLocator.
Step 1 - Scopri quale tipo di visualizzazione è stato costruito.
Step 2 - Identifica il ViewModel per quel particolare tipo di vista.
Step 3 - Costruisci quel ViewModel.
Step 4 - Imposta Views DataContext su ViewModel.
Per comprendere il concetto di base, diamo uno sguardo al semplice esempio di ViewModelLocator continuando lo stesso esempio dell'ultimo capitolo. Se guardi il file StudentView.xaml, vedrai che abbiamo collegato staticamente il ViewModel.
Ora, come mostrato nel programma seguente, commenta questo codice XAML e rimuovi anche il codice da Code-behind.
<UserControl x:Class = "MVVMDemo.Views.StudentView"
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:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
<!--<UserControl.DataContext>
<viewModel:StudentViewModel/>
</UserControl.DataContext>-->
<Grid>
<StackPanel HorizontalAlignment = "Left">
<ItemsControl ItemsSource = "{Binding Path = Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
Ora creiamo una nuova cartella VML e aggiungiamo una nuova classe pubblica ViewModelLocator che conterrà una singola proprietà allegata (proprietà di dipendenza) AutoHookedUpViewModel come mostrato nel codice seguente.
public static bool GetAutoHookedUpViewModel(DependencyObject obj) {
return (bool)obj.GetValue(AutoHookedUpViewModelProperty);
}
public static void SetAutoHookedUpViewModel(DependencyObject obj, bool value) {
obj.SetValue(AutoHookedUpViewModelProperty, value);
}
// Using a DependencyProperty as the backing store for AutoHookedUpViewModel.
//This enables animation, styling, binding, etc...
public static readonly DependencyProperty AutoHookedUpViewModelProperty =
DependencyProperty.RegisterAttached("AutoHookedUpViewModel",
typeof(bool), typeof(ViewModelLocator), new PropertyMetadata(false,
AutoHookedUpViewModelChanged));
E ora puoi vedere una definizione di proprietà di collegamento di base. Per aggiungere un comportamento alla proprietà, dobbiamo aggiungere un gestore di eventi modificato per questa proprietà che contiene il processo automatico di collegamento del ViewModel per la visualizzazione. Il codice per farlo è il seguente:
private static void AutoHookedUpViewModelChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e) {
if (DesignerProperties.GetIsInDesignMode(d)) return;
var viewType = d.GetType();
string str = viewType.FullName;
str = str.Replace(".Views.", ".ViewModel.");
var viewTypeName = str;
var viewModelTypeName = viewTypeName + "Model";
var viewModelType = Type.GetType(viewModelTypeName);
var viewModel = Activator.CreateInstance(viewModelType);
((FrameworkElement)d).DataContext = viewModel;
}
Di seguito è riportata l'implementazione completa della classe ViewModelLocator.
using System;
using System.ComponentModel;
using System.Windows;
namespace MVVMDemo.VML {
public static class ViewModelLocator {
public static bool GetAutoHookedUpViewModel(DependencyObject obj) {
return (bool)obj.GetValue(AutoHookedUpViewModelProperty);
}
public static void SetAutoHookedUpViewModel(DependencyObject obj, bool value) {
obj.SetValue(AutoHookedUpViewModelProperty, value);
}
// Using a DependencyProperty as the backing store for AutoHookedUpViewModel.
//This enables animation, styling, binding, etc...
public static readonly DependencyProperty AutoHookedUpViewModelProperty =
DependencyProperty.RegisterAttached("AutoHookedUpViewModel",
typeof(bool), typeof(ViewModelLocator), new
PropertyMetadata(false, AutoHookedUpViewModelChanged));
private static void AutoHookedUpViewModelChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e) {
if (DesignerProperties.GetIsInDesignMode(d)) return;
var viewType = d.GetType();
string str = viewType.FullName;
str = str.Replace(".Views.", ".ViewModel.");
var viewTypeName = str;
var viewModelTypeName = viewTypeName + "Model";
var viewModelType = Type.GetType(viewModelTypeName);
var viewModel = Activator.CreateInstance(viewModelType);
((FrameworkElement)d).DataContext = viewModel;
}
}
}
La prima cosa da fare è aggiungere uno spazio dei nomi in modo da poter accedere a quel tipo ViewModelLocator nella radice del nostro progetto. Quindi sull'elemento route che è un tipo di visualizzazione, aggiungi la proprietà AutoHookedUpViewModel e impostala su true.
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
Ecco l'implementazione completa del file StudentView.xaml.
<UserControl x:Class = "MVVMDemo.Views.StudentView"
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:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
<!--<UserControl.DataContext>
<viewModel:StudentViewModel/>
</UserControl.DataContext>-->
<Grid>
<StackPanel HorizontalAlignment = "Left">
<ItemsControl ItemsSource = "{Binding Path = Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
Quando il codice precedente viene compilato ed eseguito, vedrai che ViewModelLocator sta collegando il ViewModel per quella particolare vista.
Una cosa fondamentale da notare in questo caso è che la vista non è più accoppiata in un modo al tipo di ViewModel o al modo in cui viene costruita. È stato tutto spostato nella posizione centrale all'interno di ViewModelLocator.
In questo capitolo impareremo come il data binding supporta il pattern MVVM. Il data binding è la caratteristica chiave che differenzia MVVM da altri modelli di separazione dell'interfaccia utente come MVC e MVP.
Per l'associazione dati è necessario disporre di una vista o di un set di elementi dell'interfaccia utente costruiti, quindi è necessario un altro oggetto a cui punteranno le associazioni.
Gli elementi dell'interfaccia utente in una vista sono associati alle proprietà esposte da ViewModel.
L'ordine in cui sono costruiti View e ViewModel dipende dalla situazione, poiché abbiamo trattato prima la View.
Vengono creati View e ViewModel e il DataContext della View viene impostato su ViewModel.
Le associazioni possono essere associazioni di dati OneWay o TwoWay per far scorrere i dati avanti e indietro tra View e ViewModel.
Diamo uno sguardo alle associazioni di dati nello stesso esempio. Di seguito è riportato il codice XAML di StudentView.
<UserControl x:Class = "MVVMDemo.Views.StudentView"
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:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
<!--<UserControl.DataContext>
<viewModel:StudentViewModel/>
</UserControl.DataContext>-->
<Grid>
<StackPanel HorizontalAlignment = "Left">
<ItemsControl ItemsSource = "{Binding Path = Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
Se guardi il codice XAML sopra, vedrai che ItemsControl è associato alla raccolta Students esposta da ViewModel.
Puoi anche vedere che la proprietà del modello Student ha anche le proprie associazioni individuali e queste sono associate alle Textboxes e TextBlock.
ItemSource di ItemsControl è in grado di eseguire il binding alla proprietà Students, perché il DataContext complessivo per la visualizzazione è impostato su ViewModel.
Le singole associazioni delle proprietà qui sono anche associazioni DataContext, ma non sono vincolanti al ViewModel stesso, a causa del modo in cui funziona un ItemSource.
Quando un'origine dell'elemento si associa alla sua raccolta, esegue il rendering di un contenitore per ogni elemento al momento del rendering e imposta il DataContext di quel contenitore sull'elemento. Quindi il DataContext complessivo per ogni casella di testo e blocco di testo all'interno di una riga sarà un singolo Studente nella raccolta. E puoi anche vedere che queste associazioni per TextBoxes sono l'associazione dati TwoWay e per TextBlock è l'associazione dati OneWay poiché non puoi modificare TextBlock.
Quando esegui di nuovo questa applicazione, vedrai il seguente output.
Cambiamo ora il testo nella seconda casella di testo della prima riga da Allain a Upston e premiamo tab per perdere il focus. Vedrai che anche il testo TextBlock viene aggiornato.
Questo perché le associazioni dei TextBox sono impostate su TwoWay e aggiorna anche il Modello, e dal modello viene nuovamente aggiornato il TextBlock.
Un modello descrive l'aspetto generale e l'aspetto visivo del controllo. Ad ogni controllo è associato un modello predefinito che conferisce l'aspetto a quel controllo. Nell'applicazione WPF è possibile creare facilmente i propri modelli quando si desidera personalizzare il comportamento visivo e l'aspetto visivo di un controllo. La connettività tra la logica e il modello può essere ottenuta mediante l'associazione dei dati.
In MVVM, esiste un'altra forma primaria nota come prima costruzione ViewModel.
Il primo approccio alla costruzione di ViewModel sfrutta le funzionalità dei modelli di dati impliciti in WPF.
I modelli di dati impliciti possono selezionare automaticamente un modello appropriato dal dizionario delle risorse corrente per un elemento che utilizza l'associazione dati. Lo fanno in base al tipo di oggetto dati di cui viene eseguito il rendering dal data binding. Innanzitutto, è necessario disporre di un elemento che si lega a un oggetto dati.
Diamo nuovamente un'occhiata al nostro semplice esempio in cui capirai come puoi visualizzare prima il modello sfruttando i modelli di dati, in particolare i modelli di dati impliciti. Ecco l'implementazione della nostra classe StudentViewModel.
using MVVMDemo.Model;
using System.Collections.ObjectModel;
namespace MVVMDemo.ViewModel {
public class StudentViewModel {
public StudentViewModel() {
LoadStudents();
}
public ObservableCollection<Student> Students {
get;
set;
}
public void LoadStudents() {
ObservableCollection<Student> students = new ObservableCollection<Student>();
students.Add(new Student { FirstName = "Mark", LastName = "Allain" });
students.Add(new Student { FirstName = "Allen", LastName = "Brown" });
students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" });
Students = students;
}
}
}
Puoi vedere che il ViewModel sopra è invariato. Continueremo con lo stesso esempio del capitolo precedente. Questa classe ViewModel espone semplicemente la proprietà della raccolta Students e la popola durante la costruzione. Andiamo al file StudentView.xaml, rimuoviamo l'implementazione esistente e definiamo un modello di dati nella sezione Risorse.
<UserControl.Resources>
<DataTemplate x:Key = "studentsTemplate">
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
Ora aggiungi una casella di riepilogo e associa i dati a tale casella di riepilogo alla proprietà Studenti come mostrato nel codice seguente.
<ListBox ItemsSource = "{Binding Students}" ItemTemplate = "{StaticResource studentsTemplate}"/>
Nella sezione Resource, il DataTemplate ha una chiave di studentsTemplate e quindi per utilizzare effettivamente quel modello, dobbiamo usare la proprietà ItemTemplate di un ListBox. Quindi ora puoi vedere che istruiamo la casella di riepilogo a utilizzare quel modello specifico per il rendering di quegli Studenti. Di seguito è riportata l'implementazione completa del file StudentView.xaml.
<UserControl x:Class = "MVVMDemo.Views.StudentView"
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:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
<UserControl.Resources>
<DataTemplate x:Key = "studentsTemplate">
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid>
<ListBox
ItemsSource = "{Binding Students}"
ItemTemplate = "{StaticResource studentsTemplate}"/>
</Grid>
</UserControl>
Quando il codice precedente viene compilato ed eseguito, vedrai la seguente finestra, che contiene un ListBox. Ogni ListBoxItem contiene i dati dell'oggetto della classe Student che vengono visualizzati in TextBlock e nelle caselle di testo.
Per renderlo un modello implicito, è necessario rimuovere la proprietà ItemTemplate da una casella di riepilogo e aggiungere una proprietà DataType nella definizione del modello come mostrato nel codice seguente.
<UserControl.Resources>
<DataTemplate DataType = "{x:Type data:Student}">
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid>
<ListBox ItemsSource = "{Binding Students}"/>
</Grid>
In DataTemplate, l'estensione di markup x: Type è molto importante che è come un tipo di operatore in XAML. Quindi, in pratica, dobbiamo puntare al tipo di dati Student che si trova nello spazio dei nomi MVVMDemo.Model. Di seguito è riportato il file XAML completo aggiornato.
<UserControl x:Class="MVVMDemo.Views.StudentView"
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:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
xmlns:data = "clr-namespace:MVVMDemo.Model"
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
<UserControl.Resources>
<DataTemplate DataType = "{x:Type data:Student}">
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid>
<ListBox ItemsSource = "{Binding Students}"/>
</Grid>
</UserControl>
Quando si esegue nuovamente questa applicazione, si otterrà comunque lo stesso rendering degli Studenti con il modello di dati perché esegue automaticamente la mappatura del tipo di oggetto di cui viene eseguito il rendering individuando il DataTemplate appropriato.
Ti consigliamo di eseguire l'esempio precedente in un metodo passo passo per una migliore comprensione.
In questo capitolo impareremo come aggiungere interattività alle applicazioni MVVM e come chiamare in modo pulito la logica. Vedrai anche che tutto questo viene fatto mantenendo l'accoppiamento libero e una buona strutturazione che è il cuore del pattern MVVM. Per capire tutto questo, impariamo prima i comandi.
Comunicazione View / ViewModel tramite comandi
Il modello di comando è stato ben documentato e utilizza spesso il modello di progettazione per un paio di decenni. In questo schema ci sono due attori principali, l'invocatore e il ricevente.
Invoker
L'invoker è un pezzo di codice che può eseguire una logica imperativa.
In genere, è un elemento dell'interfaccia utente con cui l'utente interagisce, nel contesto di un framework dell'interfaccia utente.
Potrebbe essere solo un altro pezzo di codice logico da qualche altra parte nell'applicazione.
Ricevitore
Il ricevitore è la logica destinata all'esecuzione quando l'invocatore viene attivato.
Nel contesto di MVVM, il ricevitore è in genere un metodo nel ViewModel che deve essere chiamato.
Tra questi due, c'è un livello di ostruzione, il che implica che l'invocatore e il destinatario non devono conoscersi esplicitamente l'uno dell'altro. Questo è tipicamente rappresentato come un'astrazione dell'interfaccia esposta al chiamante e un'implementazione concreta di tale interfaccia è in grado di chiamare il ricevitore.
Diamo un'occhiata a un semplice esempio in cui imparerai i comandi e come usarli per comunicare tra View e ViewModel. In questo capitolo, continueremo con lo stesso esempio dell'ultimo capitolo.
Nel file StudentView.xaml, abbiamo un ListBox che collega i dati degli studenti da un ViewModel. Ora aggiungiamo un pulsante per eliminare uno studente dalla ListBox.
La cosa importante è che lavorare con i comandi sul pulsante è molto semplice perché hanno una proprietà di comando da collegare a un ICommand.
Quindi, possiamo esporre una proprietà sul nostro ViewModel che ha un ICommand e si associa ad esso dalla proprietà del comando del pulsante come mostrato nel codice seguente.
<Button Content = "Delete"
Command = "{Binding DeleteCommand}"
HorizontalAlignment = "Left"
VerticalAlignment = "Top"
Width = "75" />
Aggiungiamo 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 MVVMDemo {
public class MyICommand : ICommand {
Action _TargetExecuteMethod;
Func<bool> _TargetCanExecuteMethod;
public MyICommand(Action executeMethod) {
_TargetExecuteMethod = executeMethod;
}
public MyICommand(Action executeMethod, Func<bool> canExecuteMethod){
_TargetExecuteMethod = executeMethod;
_TargetCanExecuteMethod = canExecuteMethod;
}
public void RaiseCanExecuteChanged() {
CanExecuteChanged(this, EventArgs.Empty);
}
bool ICommand.CanExecute(object parameter) {
if (_TargetCanExecuteMethod != null) {
return _TargetCanExecuteMethod();
}
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();
}
}
}
}
Come puoi vedere, questa è una semplice implementazione di delega di ICommand in cui abbiamo due delegati uno per executeMethod e uno per canExecuteMethod che possono essere passati alla costruzione.
Nell'implementazione precedente, ci sono due costruttori sovraccaricati, uno per solo executeMethod e uno per executeMethod e I canExecuteMethod.
Aggiungiamo una proprietà di tipo MyICommand nella classe StudentView Model. Ora dobbiamo costruire un'istanza in StudentViewModel. Useremo il costruttore sovraccarico di MyICommand che accetta due parametri.
public MyICommand DeleteCommand { get; set;}
public StudentViewModel() {
LoadStudents();
DeleteCommand = new MyICommand(OnDelete, CanDelete);
}
Ora aggiungi l'implementazione dei metodi OnDelete e CanDelete.
private void OnDelete() {
Students.Remove(SelectedStudent);
}
private bool CanDelete() {
return SelectedStudent != null;
}
È inoltre necessario aggiungere un nuovo SelectedStudent in modo che l'utente possa eliminare l'elemento selezionato da ListBox.
private Student _selectedStudent;
public Student SelectedStudent {
get {
return _selectedStudent;
}
set {
_selectedStudent = value;
DeleteCommand.RaiseCanExecuteChanged();
}
}
Di seguito è riportata l'implementazione completa della classe ViewModel.
using MVVMDemo.Model;
using System.Collections.ObjectModel;
using System.Windows.Input;
using System;
namespace MVVMDemo.ViewModel {
public class StudentViewModel {
public MyICommand DeleteCommand { get; set;}
public StudentViewModel() {
LoadStudents();
DeleteCommand = new MyICommand(OnDelete, CanDelete);
}
public ObservableCollection<Student> Students {
get;
set;
}
public void LoadStudents() {
ObservableCollection<Student> students = new ObservableCollection<Student>();
students.Add(new Student { FirstName = "Mark", LastName = "Allain" });
students.Add(new Student { FirstName = "Allen", LastName = "Brown" });
students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" });
Students = students;
}
private Student _selectedStudent;
public Student SelectedStudent {
get {
return _selectedStudent;
}
set {
_selectedStudent = value;
DeleteCommand.RaiseCanExecuteChanged();
}
}
private void OnDelete() {
Students.Remove(SelectedStudent);
}
private bool CanDelete() {
return SelectedStudent != null;
}
}
}
In StudentView.xaml, dobbiamo aggiungere la proprietà SelectedItem in una ListBox che si legherà alla proprietà SelectStudent.
<ListBox ItemsSource = "{Binding Students}" SelectedItem = "{Binding SelectedStudent}"/>
Di seguito è riportato il file xaml completo.
<UserControl x:Class = "MVVMDemo.Views.StudentView"
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:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
xmlns:data = "clr-namespace:MVVMDemo.Model"
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<UserControl.Resources>
<DataTemplate DataType = "{x:Type data:Student}">
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid>
<StackPanel Orientation = "Horizontal">
<ListBox ItemsSource = "{Binding Students}"
SelectedItem = "{Binding SelectedStudent}"/>
<Button Content = "Delete"
Command = "{Binding DeleteCommand}"
HorizontalAlignment = "Left"
VerticalAlignment = "Top"
Width = "75" />
</StackPanel>
</Grid>
</UserControl>
Quando il codice sopra è stato compilato ed eseguito, vedrai la seguente finestra.
Puoi vedere che il pulsante Elimina è disabilitato. Sarà abilitato quando selezioni un elemento.
Quando selezioni un elemento e premi Elimina. Vedrai che l'elenco degli elementi selezionati viene eliminato e il pulsante Elimina viene nuovamente disabilitato.
Ti consigliamo di eseguire l'esempio precedente in modo graduale per una migliore comprensione.
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 si dispone di 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 ViewModel 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.
In questo capitolo impareremo le convalide. Esamineremo anche un modo pulito per eseguire la convalida con ciò che le associazioni WPF supportano già, ma collegandolo ai componenti MVVM.
Validazione in MVVM
Quando l'applicazione inizia ad accettare l'input di dati dagli utenti finali, è necessario considerare la convalida di tale input.
Assicurati che sia conforme ai tuoi requisiti generali.
WPF ha alcune fantastiche build e funzionalità nel sistema di associazione per la convalida dell'input e puoi comunque sfruttare tutte queste funzionalità quando esegui MVVM.
Tieni presente che la logica che supporta la tua convalida e definisce quali regole esistono per quali proprietà dovrebbero essere parte del Model o del ViewModel, non della View stessa.
È comunque possibile utilizzare tutti i modi per esprimere la convalida supportati dall'associazione dati WPF, inclusi:
- Viene impostato il lancio di eccezioni su una proprietà.
- Implementazione dell'interfaccia IDataErrorInfo.
- Implementazione di INotifyDataErrorInfo.
- Usa regole di convalida WPF.
In generale, INotifyDataErrorInfo è consigliato ed è stato introdotto in WPF .net 4.5 e supporta l'interrogazione dell'oggetto per errori associati alle proprietà e risolve anche un paio di carenze con tutte le altre opzioni. In particolare, consente la convalida asincrona. Consente alle proprietà di avere più di un errore associato.
Aggiunta di convalida
Diamo un'occhiata a un esempio in cui aggiungeremo il supporto di convalida alla nostra vista di input, e in un'applicazione di grandi dimensioni probabilmente avrai bisogno di questo in un certo numero di punti nella tua applicazione. A volte su Views, a volte su ViewModels e talvolta su questi oggetti helper ci sono wrapper attorno agli oggetti del modello.
È consigliabile inserire il supporto di convalida in una classe base comune che è possibile ereditare da diversi scenari.
La classe base supporterà INotifyDataErrorInfo in modo che la convalida venga attivata quando le proprietà cambiano.
Crea aggiunge una nuova classe chiamata ValidatableBindableBase. Poiché abbiamo già una classe base per la gestione di una modifica di proprietà, deriviamo la classe base da essa e implementiamo anche l'interfaccia INotifyDataErrorInfo.
Di seguito è riportata l'implementazione della classe ValidatableBindableBase.
using System;
using System.Collections.Generic;
using System.ComponentModel;
//using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
namespace MVVMHierarchiesDemo {
public class ValidatableBindableBase : BindableBase, INotifyDataErrorInfo {
private Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>();
public event EventHandler<DataErrorsChangedEventArgs>
ErrorsChanged = delegate { };
public System.Collections.IEnumerable GetErrors(string propertyName) {
if (_errors.ContainsKey(propertyName))
return _errors[propertyName];
else
return null;
}
public bool HasErrors {
get { return _errors.Count > 0; }
}
protected override void SetProperty<T>(ref T member, T val,
[CallerMemberName] string propertyName = null) {
base.SetProperty<T>(ref member, val, propertyName);
ValidateProperty(propertyName, val);
}
private void ValidateProperty<T>(string propertyName, T value) {
var results = new List<ValidationResult>();
//ValidationContext context = new ValidationContext(this);
//context.MemberName = propertyName;
//Validator.TryValidateProperty(value, context, results);
if (results.Any()) {
//_errors[propertyName] = results.Select(c => c.ErrorMessage).ToList();
} else {
_errors.Remove(propertyName);
}
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
}
}
Ora aggiungi AddEditCustomerView e AddEditCustomerViewModel nelle rispettive cartelle. Di seguito è riportato il codice di AddEditCustomerView.xaml.
<UserControl x:Class = "MVVMHierarchiesDemo.Views.AddEditCustomerView"
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>
<Grid.RowDefinitions>
<RowDefinition Height = "Auto" />
<RowDefinition Height = "Auto" />
</Grid.RowDefinitions>
<Grid x:Name = "grid1"
HorizontalAlignment = "Left"
DataContext = "{Binding Customer}"
Margin = "10,10,0,0"
VerticalAlignment = "Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width = "Auto" />
<ColumnDefinition Width = "Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height = "Auto" />
<RowDefinition Height = "Auto" />
<RowDefinition Height = "Auto" />
<RowDefinition Height = "Auto" />
</Grid.RowDefinitions>
<Label Content = "First Name:"
Grid.Column = "0"
HorizontalAlignment = "Left"
Margin = "3"
Grid.Row = "0"
VerticalAlignment = "Center" />
<TextBox x:Name = "firstNameTextBox"
Grid.Column = "1"
HorizontalAlignment = "Left"
Height = "23"
Margin = "3"
Grid.Row = "0"
Text = "{Binding FirstName, ValidatesOnNotifyDataErrors = True}"
VerticalAlignment = "Center"
Width = "120" />
<Label Content = "Last Name:"
Grid.Column = "0"
HorizontalAlignment = "Left"
Margin = "3"
Grid.Row = "1"
VerticalAlignment = "Center" />
<TextBox x:Name = "lastNameTextBox"
Grid.Column = "1"
HorizontalAlignment = "Left"
Height = "23"
Margin = "3"
Grid.Row = "1"
Text = "{Binding LastName, ValidatesOnNotifyDataErrors = True}"
VerticalAlignment = "Center"
Width = "120" />
<Label Content = "Email:"
Grid.Column = "0"
HorizontalAlignment = "Left"
Margin = "3"
Grid.Row = "2"
VerticalAlignment = "Center" />
<TextBox x:Name = "emailTextBox"
Grid.Column = "1"
HorizontalAlignment = "Left"
Height = "23"
Margin = "3"
Grid.Row = "2"
Text = "{Binding Email, ValidatesOnNotifyDataErrors = True}"
VerticalAlignment = "Center"
Width = "120" />
<Label Content = "Phone:"
Grid.Column = "0"
HorizontalAlignment = "Left"
Margin = "3"
Grid.Row = "3"
VerticalAlignment = "Center" />
<TextBox x:Name = "phoneTextBox"
Grid.Column = "1"
HorizontalAlignment = "Left"
Height = "23"
Margin = "3"
Grid.Row = "3"
Text = "{Binding Phone, ValidatesOnNotifyDataErrors = True}"
VerticalAlignment = "Center"
Width = "120" />
</Grid>
<Grid Grid.Row = "1">
<Button Content = "Save"
Command = "{Binding SaveCommand}"
HorizontalAlignment = "Left"
Margin = "25,5,0,0"
VerticalAlignment = "Top"
Width = "75" />
<Button Content = "Add"
Command = "{Binding SaveCommand}"
HorizontalAlignment = "Left"
Margin = "25,5,0,0"
VerticalAlignment = "Top"
Width = "75" />
<Button Content = "Cancel"
Command = "{Binding CancelCommand}"
HorizontalAlignment = "Left"
Margin = "150,5,0,0"
VerticalAlignment = "Top"
Width = "75" />
</Grid>
</Grid>
</UserControl>
Di seguito è riportata l'implementazione di AddEditCustomerViewModel.
using MVVMHierarchiesDemo.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo.ViewModel {
class AddEditCustomerViewModel : BindableBase {
public AddEditCustomerViewModel() {
CancelCommand = new MyIcommand(OnCancel);
SaveCommand = new MyIcommand(OnSave, CanSave);
}
private bool _EditMode;
public bool EditMode {
get { return _EditMode; }
set { SetProperty(ref _EditMode, value);}
}
private SimpleEditableCustomer _Customer;
public SimpleEditableCustomer Customer {
get { return _Customer; }
set { SetProperty(ref _Customer, value);}
}
private Customer _editingCustomer = null;
public void SetCustomer(Customer cust) {
_editingCustomer = cust;
if (Customer != null) Customer.ErrorsChanged -= RaiseCanExecuteChanged;
Customer = new SimpleEditableCustomer();
Customer.ErrorsChanged += RaiseCanExecuteChanged;
CopyCustomer(cust, Customer);
}
private void RaiseCanExecuteChanged(object sender, EventArgs e) {
SaveCommand.RaiseCanExecuteChanged();
}
public MyIcommand CancelCommand { get; private set; }
public MyIcommand SaveCommand { get; private set; }
public event Action Done = delegate { };
private void OnCancel() {
Done();
}
private async void OnSave() {
Done();
}
private bool CanSave() {
return !Customer.HasErrors;
}
}
}
Di seguito è riportata l'implementazione della classe SimpleEditableCustomer.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo.Model {
public class SimpleEditableCustomer : ValidatableBindableBase {
private Guid _id;
public Guid Id {
get { return _id; }
set { SetProperty(ref _id, value); }
}
private string _firstName;
[Required]
public string FirstName {
get { return _firstName; }
set { SetProperty(ref _firstName, value); }
}
private string _lastName;
[Required]
public string LastName {
get { return _lastName; }
set { SetProperty(ref _lastName, value); }
}
private string _email;
[EmailAddress]
public string Email {
get { return _email; }
set { SetProperty(ref _email, value); }
}
private string _phone;
[Phone]
public string Phone {
get { return _phone; }
set { SetProperty(ref _phone, value); }
}
}
}
Quando il codice sopra è stato compilato ed eseguito, vedrai la seguente finestra.
Quando premi il pulsante Aggiungi cliente vedrai la seguente vista. Quando l'utente lascia un campo vuoto, questo verrà evidenziato e il pulsante di salvataggio verrà disabilitato.
In questo capitolo, discuteremo brevemente sull'iniezione di dipendenze. Abbiamo già coperto l'associazione dei dati che separa le viste e i ViewModels l'una dall'altra, consentendo loro di comunicare senza sapere esplicitamente cosa sta succedendo all'altra estremità della comunicazione.
Ora abbiamo bisogno di qualcosa di simile per separare il nostro ViewModel dai servizi client.
Agli albori della programmazione orientata agli oggetti, gli sviluppatori hanno dovuto affrontare il problema della creazione e del recupero di istanze di classi nelle applicazioni. Diverse soluzioni sono state proposte per questo problema.
Negli ultimi anni, l'inserimento delle dipendenze e l'inversione del controllo (IoC) hanno guadagnato popolarità tra gli sviluppatori e hanno avuto la precedenza su alcune soluzioni meno recenti come il pattern Singleton.
Iniezione di dipendenze / contenitori IoC
IoC e l'inserimento delle dipendenze sono due modelli di progettazione strettamente correlati e il contenitore è fondamentalmente un pezzo di codice dell'infrastruttura che esegue entrambi questi modelli per te.
Il modello IoC riguarda la delega della responsabilità per la costruzione e il modello di inserimento delle dipendenze riguarda la fornitura di dipendenze a un oggetto che è già stato costruito.
Entrambi possono essere trattati come un approccio in due fasi alla costruzione. Quando si utilizza un contenitore, il contenitore assume diverse responsabilità che sono le seguenti:
- Costruisce un oggetto quando richiesto.
- Il contenitore determinerà da cosa dipende quell'oggetto.
- Costruire quelle dipendenze.
- Iniettandoli nell'oggetto in costruzione.
- Processo ricorsivo.
Diamo un'occhiata a come possiamo usare l'inserimento delle dipendenze per interrompere il disaccoppiamento tra ViewModels e i servizi client. Collegheremo il modulo AddEditCustomerViewModel per la gestione del salvataggio utilizzando l'inserimento delle dipendenze correlato a questo.
Per prima cosa dobbiamo creare una nuova interfaccia nel nostro progetto nella cartella Servizi. Se non hai una cartella dei servizi nel tuo progetto, creala prima e aggiungi la seguente interfaccia nella cartella Servizi.
using MVVMHierarchiesDemo.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo.Services {
public interface ICustomersRepository {
Task<List<Customer>> GetCustomersAsync();
Task<Customer> GetCustomerAsync(Guid id);
Task<Customer> AddCustomerAsync(Customer customer);
Task<Customer> UpdateCustomerAsync(Customer customer);
Task DeleteCustomerAsync(Guid customerId);
}
}
Di seguito è riportata l'implementazione di ICustomersRepository.
using MVVMHierarchiesDemo.Model;
using System;
using System.Collections.Generic;
using System.Linq; using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo.Services {
public class CustomersRepository : ICustomersRepository {
ZzaDbContext _context = new ZzaDbContext();
public Task<List<Customer>> GetCustomersAsync() {
return _context.Customers.ToListAsync();
}
public Task<Customer> GetCustomerAsync(Guid id) {
return _context.Customers.FirstOrDefaultAsync(c => c.Id == id);
}
public async Task<Customer> AddCustomerAsync(Customer customer){
_context.Customers.Add(customer);
await _context.SaveChangesAsync();
return customer;
}
public async Task<Customer> UpdateCustomerAsync(Customer customer) {
if (!_context.Customers.Local.Any(c => c.Id == customer.Id)) {
_context.Customers.Attach(customer);
}
_context.Entry(customer).State = EntityState.Modified;
await _context.SaveChangesAsync();
return customer;
}
public async Task DeleteCustomerAsync(Guid customerId) {
var customer = _context.Customers.FirstOrDefault(c => c.Id == customerId);
if (customer != null) {
_context.Customers.Remove(customer);
}
await _context.SaveChangesAsync();
}
}
}
Il modo semplice per eseguire la gestione del salvataggio è aggiungere una nuova istanza di ICustomersRepository in AddEditCustomerViewModel e sovraccaricare il costruttore AddEditCustomerViewModel e CustomerListViewModel.
private ICustomersRepository _repo;
public AddEditCustomerViewModel(ICustomersRepository repo) {
_repo = repo;
CancelCommand = new MyIcommand(OnCancel);
SaveCommand = new MyIcommand(OnSave, CanSave);
}
Aggiorna il metodo OnSave come mostrato nel codice seguente.
private async void OnSave() {
UpdateCustomer(Customer, _editingCustomer);
if (EditMode)
await _repo.UpdateCustomerAsync(_editingCustomer);
else
await _repo.AddCustomerAsync(_editingCustomer);
Done();
}
private void UpdateCustomer(SimpleEditableCustomer source, Customer target) {
target.FirstName = source.FirstName;
target.LastName = source.LastName;
target.Phone = source.Phone;
target.Email = source.Email;
}
Di seguito è riportato il AddEditCustomerViewModel completo.
using MVVMHierarchiesDemo.Model;
using MVVMHierarchiesDemo.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo.ViewModel {
class AddEditCustomerViewModel : BindableBase {
private ICustomersRepository _repo;
public AddEditCustomerViewModel(ICustomersRepository repo) {
_repo = repo;
CancelCommand = new MyIcommand(OnCancel);
SaveCommand = new MyIcommand(OnSave, CanSave);
}
private bool _EditMode;
public bool EditMode {
get { return _EditMode; }
set { SetProperty(ref _EditMode, value); }
}
private SimpleEditableCustomer _Customer;
public SimpleEditableCustomer Customer {
get { return _Customer; }
set { SetProperty(ref _Customer, value); }
}
private Customer _editingCustomer = null;
public void SetCustomer(Customer cust) {
_editingCustomer = cust;
if (Customer != null) Customer.ErrorsChanged -= RaiseCanExecuteChanged;
Customer = new SimpleEditableCustomer();
Customer.ErrorsChanged += RaiseCanExecuteChanged;
CopyCustomer(cust, Customer);
}
private void RaiseCanExecuteChanged(object sender, EventArgs e) {
SaveCommand.RaiseCanExecuteChanged();
}
public MyIcommand CancelCommand { get; private set; }
public MyIcommand SaveCommand { get; private set; }
public event Action Done = delegate { };
private void OnCancel() {
Done();
}
private async void OnSave() {
UpdateCustomer(Customer, _editingCustomer);
if (EditMode)
await _repo.UpdateCustomerAsync(_editingCustomer);
else
await _repo.AddCustomerAsync(_editingCustomer);
Done();
}
private void UpdateCustomer(SimpleEditableCustomer source, Customer target) {
target.FirstName = source.FirstName;
target.LastName = source.LastName;
target.Phone = source.Phone;
target.Email = source.Email;
}
private bool CanSave() {
return !Customer.HasErrors;
}
private void CopyCustomer(Customer source, SimpleEditableCustomer target) {
target.Id = source.Id;
if (EditMode) {
target.FirstName = source.FirstName;
target.LastName = source.LastName;
target.Phone = source.Phone;
target.Email = source.Email;
}
}
}
}
Quando il codice sopra viene compilato ed eseguito, vedrai lo stesso output ma ora i ViewModels sono disaccoppiati in modo più lasco.
Quando premi il pulsante Aggiungi cliente, vedrai la seguente vista. Quando l'utente lascia un campo vuoto, questo verrà evidenziato e il pulsante di salvataggio verrà disabilitato.
Un evento è un costrutto di programmazione che reagisce a un cambiamento di stato, notificando tutti gli endpoint che si sono registrati per la notifica. In primo luogo, gli eventi vengono utilizzati per informare un input dell'utente tramite mouse e tastiera, ma la loro utilità non si limita a questo. Ogni volta che viene rilevato un cambiamento di stato, magari quando un oggetto è stato caricato o inizializzato, può essere attivato un evento per avvisare eventuali terze parti interessate.
In un'applicazione WPF che usa il modello di progettazione MVVM (Model-View-ViewModel), il modello di visualizzazione è il componente responsabile della gestione della logica e dello stato di presentazione dell'applicazione.
Il file code-behind della vista non deve contenere codice per gestire gli eventi generati da qualsiasi elemento dell'interfaccia utente (UI) come un pulsante o un controllo ComboBox né deve contenere alcuna logica specifica del dominio.
Idealmente, il code-behind di una vista contiene solo un costruttore che chiama il metodo InitializeComponent e forse un po 'di codice aggiuntivo per controllare o interagire con il livello di visualizzazione che è difficile o inefficiente da esprimere in XAML, ad esempio animazioni complesse.
Diamo un'occhiata a un semplice esempio di eventi di clic sui pulsanti nella nostra applicazione. Di seguito è riportato il codice XAML del file MainWindow.xaml in cui vedrai due pulsanti.
<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>
Puoi vedere che la proprietà Click del pulsante non viene utilizzata nel file XAML precedente, ma le proprietà Command e CommandParameter vengono usate per caricare visualizzazioni diverse quando viene premuto il pulsante. Ora è necessario definire l'implementazione dei comandi nel file MainWindowViewModel.cs ma non nel file Visualizza. 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. Ora, se fai clic su qualsiasi pulsante, passerà a quella particolare vista. Facciamo clic sul pulsante Clienti e vedrai che viene visualizzato CustomerListView.
Ti consigliamo di eseguire l'esempio precedente in un metodo passo passo per una migliore comprensione.
L'idea alla base del test unitario è quella di prendere blocchi discreti di codice (unità) e scrivere metodi di test che utilizzano il codice in un modo previsto, quindi testare per vedere se ottengono i risultati previsti.
Essendo codice stesso, i test unitari vengono compilati proprio come il resto del progetto.
Vengono anche eseguiti dal software in esecuzione del test, che può accelerare ogni test, dando effettivamente il pollice in alto o il pollice in giù per indicare se il test è stato superato o meno, rispettivamente.
Diamo un'occhiata a un esempio creato in precedenza. Di seguito è riportata l'implementazione di Student Model.
using System.ComponentModel;
namespace MVVMDemo.Model {
public class StudentModel {}
public class Student : INotifyPropertyChanged {
private string firstName;
private string lastName;
public string FirstName {
get { return firstName; }
set {
if (firstName != value) {
firstName = value;
RaisePropertyChanged("FirstName");
RaisePropertyChanged("FullName");
}
}
}
public string LastName {
get { return lastName; }
set {
if (lastName != value) {
lastName = value;
RaisePropertyChanged("LastName");
RaisePropertyChanged("FullName");
}
}
}
public string FullName {
get {
return firstName + " " + lastName;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
}
Di seguito è riportata l'implementazione di StudentView.
<UserControl x:Class="MVVMDemo.Views.StudentView"
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:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
xmlns:data = "clr-namespace:MVVMDemo.Model"
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<UserControl.Resources>
<DataTemplate DataType = "{x:Type data:Student}">
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid>
<StackPanel Orientation = "Horizontal">
<ListBox ItemsSource = "{Binding Students}"
SelectedItem = "{Binding SelectedStudent}"/>
<Button Content = "Delete"
Command = "{Binding DeleteCommand}"
HorizontalAlignment = "Left"
VerticalAlignment = "Top"
Width = "75" />
</StackPanel>
</Grid>
</UserControl>
Di seguito è riportata l'implementazione di StudentViewModel.
using MVVMDemo.Model;
using System.Collections.ObjectModel;
using System.Windows.Input;
using System;
namespace MVVMDemo.ViewModel {
public class StudentViewModel {
public MyICommand DeleteCommand { get; set;}
public StudentViewModel() {
LoadStudents();
DeleteCommand = new MyICommand(OnDelete, CanDelete);
}
public ObservableCollection<Student> Students {
get;
set;
}
public void LoadStudents() {
ObservableCollection<Student> students = new ObservableCollection<Student>();
students.Add(new Student { FirstName = "Mark", LastName = "Allain" });
students.Add(new Student { FirstName = "Allen", LastName = "Brown" });
students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" });
Students = students;
}
private Student _selectedStudent;
public Student SelectedStudent {
get {
return _selectedStudent;
}
set {
_selectedStudent = value;
DeleteCommand.RaiseCanExecuteChanged();
}
}
private void OnDelete() {
Students.Remove(SelectedStudent);
}
private bool CanDelete() {
return SelectedStudent != null;
}
public int GetStudentCount() {
return Students.Count;
}
}
}
Di seguito è riportato il file MainWindow.xaml.
<Window x:Class = "MVVMDemo.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:MVVMDemo"
xmlns:views = "clr-namespace:MVVMDemo.Views"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Grid>
<views:StudentView x:Name = "StudentViewControl"/>
</Grid>
</Window>
Di seguito è riportata l'implementazione di MyICommand, che implementa l'interfaccia ICommand.
using System;
using System.Windows.Input;
namespace MVVMDemo {
public class MyICommand : ICommand {
Action _TargetExecuteMethod;
Func<bool> _TargetCanExecuteMethod;
public MyICommand(Action executeMethod) {
_TargetExecuteMethod = executeMethod;
}
public MyICommand(Action executeMethod, Func<bool> canExecuteMethod) {
_TargetExecuteMethod = executeMethod;
_TargetCanExecuteMethod = canExecuteMethod;
}
public void RaiseCanExecuteChanged() {
CanExecuteChanged(this, EventArgs.Empty);
}
bool ICommand.CanExecute(object parameter) {
if (_TargetCanExecuteMethod != null) {
return _TargetCanExecuteMethod();
}
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();
}
}
}
}
Quando il codice sopra è stato compilato ed eseguito, vedrai il seguente output nella finestra principale.
Per scrivere uno unit test per l'esempio precedente, aggiungiamo un nuovo progetto di test alla soluzione.
Aggiungi riferimento al progetto facendo clic con il pulsante destro del mouse su Riferimenti.
Seleziona il progetto esistente e fai clic su Ok.
Aggiungiamo ora un semplice test che controllerà il conteggio degli studenti come mostrato nel codice seguente.
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MVVMDemo.ViewModel;
namespace MVVMTest {
[TestClass]
public class UnitTest1 {
[TestMethod]
public void TestMethod1() {
StudentViewModel sViewModel = new StudentViewModel();
int count = sViewModel.GetStudentCount();
Assert.IsTrue(count == 3);
}
}
}
Per eseguire questo test, selezionare l'opzione di menu Test → Esegui → Tutti i test.
Puoi vedere in Esplora test che il test è stato superato, perché in StudentViewModel vengono aggiunti tre studenti. Modificare la condizione di conteggio da 3 a 4 come mostrato nel codice seguente.
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MVVMDemo.ViewModel;
namespace MVVMTest {
[TestClass]
public class UnitTest1 {
[TestMethod] public void TestMethod1() {
StudentViewModel sViewModel = new StudentViewModel();
int count = sViewModel.GetStudentCount();
Assert.IsTrue(count == 4);
}
}
}
Quando il piano di test viene eseguito di nuovo, vedrai che il test è fallito perché il conteggio degli studenti non è uguale a 4.
Ti consigliamo di eseguire l'esempio precedente in un metodo passo passo per una migliore comprensione.
In questo capitolo, discuteremo dei toolkit o dei framework MVVM disponibili. Puoi anche usare questi framework in modo da non dover scrivere un mucchio di codice ripetitivo per implementare tu stesso il pattern MVVM. Ecco alcuni dei framework più popolari:
- Prism
- MVVM Light
- Caliburn Micro
Prisma
Prism fornisce indicazioni sotto forma di esempi e documentazione che consentono di progettare e creare facilmente applicazioni desktop Windows Presentation Foundation (WPF) ricche, flessibili e di facile manutenzione. Rich Internet Applications (RIA) create con il plug-in del browser Microsoft Silverlight e le applicazioni Windows.
Prism utilizza modelli di progettazione che incarnano importanti principi di progettazione architettonica, come la separazione delle preoccupazioni e l'accoppiamento libero.
Prism ti aiuta a progettare e creare applicazioni utilizzando componenti liberamente accoppiati che possono evolversi in modo indipendente ma che possono essere facilmente e perfettamente integrati nell'applicazione complessiva.
Questi tipi di applicazioni sono noti come applicazioni composite.
Prism ha una serie di funzioni pronte all'uso. Di seguito sono riportate alcune delle caratteristiche importanti di Prism.
Pattern MVVM
Prism ha il supporto per il pattern MVVM. Ha una classe Bindablebase simile a quella implementata nei capitoli precedenti.
Ha un ViewModelLocator flessibile che ha delle convenzioni ma consente di sovrascrivere tali convenzioni e collegare in modo dichiarativo le visualizzazioni e i ViewModel in modo liberamente accoppiato.
Modularità
È la capacità di suddividere il codice in librerie di classi totalmente liberamente accoppiate in parti e riunirle in fase di esecuzione in un insieme coeso per l'utente finale, mentre il codice rimane completamente disaccoppiato.
Composizione / regioni dell'interfaccia utente
È la capacità di collegare le viste ai contenitori senza che la vista che esegue il collegamento, necessiti di un riferimento esplicito al contenitore dell'interfaccia utente stesso.
Navigazione
Prism ha funzionalità di navigazione che si sovrappongono alle regioni, come la navigazione in avanti e all'indietro e lo stack di navigazione che consente ai modelli di visualizzazione di partecipare direttamente al processo di navigazione.
Comandi
Prism ha comandi quindi hanno un comando delegato che è molto simile a MyICommand che abbiamo usato nei capitoli precedenti tranne che ha una maggiore robustezza per proteggerti dalle perdite di memoria.
Eventi Pub / Sub
Prism supporta anche gli eventi Pub / Sub. Si tratta di eventi liberamente accoppiati in cui l'editore e il sottoscrittore possono avere durate diverse e non devono avere riferimenti espliciti l'uno all'altro per comunicare tramite eventi.
MVVM Light
MVVM Light è prodotto da Laurent Bugnion e ti aiuta a separare la tua vista dal tuo modello, creando applicazioni più pulite e più facili da mantenere ed estendere.
Crea anche applicazioni testabili e consente di avere un livello di interfaccia utente molto più sottile (che è più difficile da testare automaticamente).
Questo toolkit pone un'enfasi particolare sull'apertura e la modifica dell'interfaccia utente in Blend, inclusa la creazione di dati in fase di progettazione per consentire agli utenti di Blend di "vedere qualcosa" quando lavorano con i controlli dei dati.
Caliburn Micro
Questo è un altro piccolo framework open source che ti aiuta a implementare il pattern MVVM e supporta anche una serie di cose out-of-the-box.
Caliburn Micro è un framework piccolo ma potente, progettato per la creazione di applicazioni su tutte le piattaforme XAML.
Con un forte supporto per MVVM e altri modelli di interfaccia utente collaudati, Caliburn Micro ti consentirà di creare rapidamente la tua soluzione, senza la necessità di sacrificare la qualità del codice o la testabilità.