Data binding e MVVM
Il modello Model-View-ViewModel (MVVM) applica una separazione tra tre livelli software, ovvero l'interfaccia utente XAML, denominata visualizzazione, i dati sottostanti, il modello e un intermediario tra la visualizzazione e il modello, detto modello di visualizzazione. La visualizzazione e il modello di visualizzazione sono spesso connessi tramite data binding definiti in XAML. L'oggetto BindingContext
per la visualizzazione è in genere un'istanza del modello di visualizzazione.
Importante
L'interfaccia utente dell'app multipiattaforma .NET (.NET MAUI) effettua il marshalling degli aggiornamenti dell'associazione al thread dell'interfaccia utente. Quando si usa MVVM, questo consente di aggiornare le proprietà del modello di visualizzazione associato a dati da qualsiasi thread, con il motore di associazione di .NET MAUI che apporta gli aggiornamenti al thread dell'interfaccia utente.
Esistono più approcci all'implementazione del modello MVVM e questo articolo è incentrato su un approccio semplice. Usa visualizzazioni e modelli di visualizzazione, ma non modelli, per concentrarsi sul data binding tra i due livelli. Per una spiegazione dettagliata dell'uso del modello MVVM in .NET MAUI, vedere Model-View-ViewModel (MVVM) in Modelli di applicazioni aziendali con .NET MAUI. Per un'esercitazione che consente di implementare il modello MVVM, vedere Aggiornare l'app con i concetti relativi a MVVM.
MVVM semplice
Nelle estensioni di markup XAML è stato illustrato come definire una nuova dichiarazione dello spazio dei nomi XML per consentire a un file XAML di fare riferimento alle classi in altri assembly. Nell'esempio seguente viene utilizzata l'estensione x:Static
di markup per ottenere la data e l'ora correnti dalla proprietà statica DateTime.Now
nello spazio dei System
nomi :
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:sys="clr-namespace:System;assembly=netstandard"
x:Class="XamlSamples.OneShotDateTimePage"
Title="One-Shot DateTime Page">
<VerticalStackLayout BindingContext="{x:Static sys:DateTime.Now}"
Spacing="25" Padding="30,0"
VerticalOptions="Center" HorizontalOptions="Center">
<Label Text="{Binding Year, StringFormat='The year is {0}'}" />
<Label Text="{Binding StringFormat='The month is {0:MMMM}'}" />
<Label Text="{Binding Day, StringFormat='The day is {0}'}" />
<Label Text="{Binding StringFormat='The time is {0:T}'}" />
</VerticalStackLayout>
</ContentPage>
In questo esempio, il valore recuperato DateTime
viene impostato come su BindingContext
un oggetto StackLayout. Quando si imposta l'oggetto BindingContext
su un elemento, viene ereditato da tutti gli elementi figlio di tale elemento. Ciò significa che tutti gli elementi figlio di StackLayout hanno lo stesso BindingContext
e possono contenere associazioni alle proprietà di tale oggetto:
Tuttavia, il problema è che la data e l'ora vengono impostate una volta quando la pagina viene costruita e inizializzata e non cambia mai.
Avviso
In una classe che deriva da BindableObject, solo le proprietà di tipo BindableProperty sono associabili. Ad esempio, VisualElement.IsLoaded e Element.Parent non sono associabili.
Una pagina XAML può visualizzare un orologio che mostra sempre l'ora corrente, ma richiede codice aggiuntivo. Il modello MVVM è una scelta naturale per le app MAUI .NET quando si esegue il data binding da proprietà tra oggetti visivi e dati sottostanti. Quando si pensa in termini di MVVM, il modello e il modello di visualizzazione sono classi scritte interamente nel codice. La visualizzazione è spesso un file XAML che fa riferimento alle proprietà definite nel modello di visualizzazione tramite data binding. In MVVM, un modello è ignorante del modello di visualizzazione e un modello di visualizzazione è ignorante della visualizzazione. Tuttavia, spesso si adattano i tipi esposti dal modello di visualizzazione ai tipi associati all'interfaccia utente.
Nota
In semplici esempi di MVVM, ad esempio quelli mostrati qui, spesso non esiste alcun modello e il modello implica solo una visualizzazione e un modello di visualizzazione collegati con i data binding.
L'esempio seguente mostra un modello di visualizzazione per un orologio, con una singola proprietà denominata DateTime
che viene aggiornata ogni secondo:
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace XamlSamples;
class ClockViewModel: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private DateTime _dateTime;
private Timer _timer;
public DateTime DateTime
{
get => _dateTime;
set
{
if (_dateTime != value)
{
_dateTime = value;
OnPropertyChanged(); // reports this property
}
}
}
public ClockViewModel()
{
this.DateTime = DateTime.Now;
// Update the DateTime property every second.
_timer = new Timer(new TimerCallback((s) => this.DateTime = DateTime.Now),
null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
}
~ClockViewModel() =>
_timer.Dispose();
public void OnPropertyChanged([CallerMemberName] string name = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
I modelli di visualizzazione implementano in genere l'interfaccia INotifyPropertyChanged
, che consente a una classe di generare l'evento PropertyChanged
ogni volta che una delle relative proprietà cambia. Il meccanismo di data binding in .NET MAUI collega un gestore a questo PropertyChanged
evento in modo che possa ricevere una notifica quando una proprietà cambia e mantenere aggiornata la destinazione con il nuovo valore. Nell'esempio di codice precedente il OnPropertyChanged
metodo gestisce la generazione dell'evento durante la determinazione automatica del nome dell'origine della proprietà: DateTime
.
L'esempio seguente mostra XAML che usa ClockViewModel
:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples"
x:Class="XamlSamples.ClockPage"
Title="Clock Page">
<ContentPage.BindingContext>
<local:ClockViewModel />
</ContentPage.BindingContext>
<Label Text="{Binding DateTime, StringFormat='{0:T}'}"
FontSize="18"
HorizontalOptions="Center"
VerticalOptions="Center" />
</ContentPage>
In questo esempio viene ClockViewModel
impostato sull'oggetto BindingContext
dei tag dell'elemento ContentPage della proprietà using. In alternativa, il file code-behind potrebbe creare un'istanza del modello di visualizzazione.
Estensione Binding
di markup nella Text
proprietà della Label proprietà formatta la DateTime
proprietà . Lo screenshot seguente mostra il risultato:
Inoltre, è possibile accedere alle singole proprietà della DateTime
proprietà del modello di visualizzazione separando le proprietà con i punti:
<Label Text="{Binding DateTime.Second, StringFormat='{0}'}" … >
MVVM interattivo
MVVM viene spesso usato con data binding bidirezionali per una visualizzazione interattiva basata su un modello di dati sottostante.
L'esempio seguente illustra l'oggetto HslViewModel
che converte un Color valore in Hue
valori , Saturation
e Luminosity
e di nuovo:
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace XamlSamples;
class HslViewModel: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private float _hue, _saturation, _luminosity;
private Color _color;
public float Hue
{
get => _hue;
set
{
if (_hue != value)
Color = Color.FromHsla(value, _saturation, _luminosity);
}
}
public float Saturation
{
get => _saturation;
set
{
if (_saturation != value)
Color = Color.FromHsla(_hue, value, _luminosity);
}
}
public float Luminosity
{
get => _luminosity;
set
{
if (_luminosity != value)
Color = Color.FromHsla(_hue, _saturation, value);
}
}
public Color Color
{
get => _color;
set
{
if (_color != value)
{
_color = value;
_hue = _color.GetHue();
_saturation = _color.GetSaturation();
_luminosity = _color.GetLuminosity();
OnPropertyChanged("Hue");
OnPropertyChanged("Saturation");
OnPropertyChanged("Luminosity");
OnPropertyChanged(); // reports this property
}
}
}
public void OnPropertyChanged([CallerMemberName] string name = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
In questo esempio, le modifiche apportate alle Hue
proprietà , Saturation
e Luminosity
causano la modifica della Color
proprietà e le modifiche apportate alla proprietà determinano la Color
modifica delle altre tre proprietà. Questo potrebbe sembrare un ciclo infinito, ad eccezione del fatto che il modello di visualizzazione non richiama l'evento PropertyChanged
a meno che la proprietà non sia stata modificata.
L'esempio XAML seguente contiene una BoxView la cui Color
proprietà è associata alla Color
proprietà del modello di visualizzazione e tre e tre Slider Label visualizzazioni associate alle Hue
proprietà , Saturation
e Luminosity
:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples"
x:Class="XamlSamples.HslColorScrollPage"
Title="HSL Color Scroll Page">
<ContentPage.BindingContext>
<local:HslViewModel Color="Aqua" />
</ContentPage.BindingContext>
<VerticalStackLayout Padding="10, 0, 10, 30">
<BoxView Color="{Binding Color}"
HeightRequest="100"
WidthRequest="100"
HorizontalOptions="Center" />
<Label Text="{Binding Hue, StringFormat='Hue = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Hue}"
Margin="20,0,20,0" />
<Label Text="{Binding Saturation, StringFormat='Saturation = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Saturation}"
Margin="20,0,20,0" />
<Label Text="{Binding Luminosity, StringFormat='Luminosity = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Luminosity}"
Margin="20,0,20,0" />
</VerticalStackLayout>
</ContentPage>
L'associazione per ogni Label è l'oggetto predefinito OneWay
. Deve solo visualizzare il valore. Tuttavia, l'associazione predefinita in ogni Slider è TwoWay
. In questo modo l'oggetto Slider può essere inizializzato dal modello di visualizzazione. Quando viene creata un'istanza del modello di visualizzazione, la proprietà è Color
impostata su Aqua
. Una modifica in un Slider imposta un nuovo valore per la proprietà nel modello di visualizzazione, che quindi calcola un nuovo colore:
Esecuzione di comandi
A volte un'app ha esigenze che vanno oltre le associazioni di proprietà richiedendo all'utente di avviare comandi che influiscono su un elemento nel modello di visualizzazione. Questi comandi vengono in genere segnalati dal clic su un pulsante o dal tocco con un dito e in genere vengono elaborati nel file code-behind in un gestore per l'evento Clicked
dell'elemento Button o per l'evento Tapped
di un elemento TapGestureRecognizer.
L'interfaccia di esecuzione dei comandi consente un approccio alternativo all'implementazione di comandi decisamente più adatto all'architettura MVVM. Il modello di visualizzazione può contenere comandi, ovvero metodi eseguiti in reazione a un'attività specifica nella visualizzazione, ad esempio un Button clic. Tra questi comandi e l'elemento Button vengono definiti data binding.
Per consentire un data binding tra un Button oggetto e un modello di visualizzazione, definisce Button due proprietà:
Command
di tipoSystem.Windows.Input.ICommand
CommandParameter
di tipoObject
Nota
Molti altri controlli definiscono Command
anche le proprietà e CommandParameter
.
L'interfaccia ICommand è definita nello spazio dei nomi System.Windows.Input ed è costituita da due metodi e da un evento:
void Execute(object arg)
bool CanExecute(object arg)
event EventHandler CanExecuteChanged
Il modello di visualizzazione può definire le proprietà di tipo ICommand. È quindi possibile associare queste proprietà alla Command
proprietà di un Button elemento o di un altro elemento o ad esempio una visualizzazione personalizzata che implementa questa interfaccia. Facoltativamente, è possibile impostare la CommandParameter
proprietà per identificare singoli Button oggetti (o altri elementi) associati a questa proprietà viewmodel. Internamente, chiama Button il Execute
metodo ogni volta che l'utente tocca , Buttonpassando al Execute
metodo il relativo CommandParameter
.
Il CanExecute
metodo e CanExecuteChanged
l'evento vengono usati per i casi in cui un Button tocco potrebbe non essere valido, nel qual caso deve Button disabilitare se stesso. Chiama Button CanExecute
quando la Command
proprietà viene impostata per la prima volta e ogni volta che viene generato l'evento CanExecuteChanged
. Se CanExecute
restituisce false
, l'oggetto Button si disabilita e non genera Execute
chiamate.
È possibile usare la Command
classe o Command<T>
inclusa in .NET MAUI per implementare l'interfaccia ICommand . Queste due classi definiscono diversi costruttori e un ChangeCanExecute
metodo che il modello di visualizzazione può chiamare per forzare l'oggetto Command
a generare l'evento CanExecuteChanged
.
L'esempio seguente mostra un modello di visualizzazione per un tastierino semplice destinato all'immissione di numeri di telefono:
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
namespace XamlSamples;
class KeypadViewModel: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _inputString = "";
private string _displayText = "";
private char[] _specialChars = { '*', '#' };
public ICommand AddCharCommand { get; private set; }
public ICommand DeleteCharCommand { get; private set; }
public string InputString
{
get => _inputString;
private set
{
if (_inputString != value)
{
_inputString = value;
OnPropertyChanged();
DisplayText = FormatText(_inputString);
// Perhaps the delete button must be enabled/disabled.
((Command)DeleteCharCommand).ChangeCanExecute();
}
}
}
public string DisplayText
{
get => _displayText;
private set
{
if (_displayText != value)
{
_displayText = value;
OnPropertyChanged();
}
}
}
public KeypadViewModel()
{
// Command to add the key to the input string
AddCharCommand = new Command<string>((key) => InputString += key);
// Command to delete a character from the input string when allowed
DeleteCharCommand =
new Command(
// Command will strip a character from the input string
() => InputString = InputString.Substring(0, InputString.Length - 1),
// CanExecute is processed here to return true when there's something to delete
() => InputString.Length > 0
);
}
string FormatText(string str)
{
bool hasNonNumbers = str.IndexOfAny(_specialChars) != -1;
string formatted = str;
// Format the string based on the type of data and the length
if (hasNonNumbers || str.Length < 4 || str.Length > 10)
{
// Special characters exist, or the string is too small or large for special formatting
// Do nothing
}
else if (str.Length < 8)
formatted = string.Format("{0}-{1}", str.Substring(0, 3), str.Substring(3));
else
formatted = string.Format("({0}) {1}-{2}", str.Substring(0, 3), str.Substring(3, 3), str.Substring(6));
return formatted;
}
public void OnPropertyChanged([CallerMemberName] string name = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
In questo esempio i Execute
metodi e CanExecute
per i comandi vengono definiti come funzioni lambda nel costruttore. Il modello di visualizzazione presuppone che la AddCharCommand
proprietà sia associata alla Command
proprietà di più pulsanti (o qualsiasi altro controllo con un'interfaccia CommandParameter
di comando), ognuna delle quali è identificata da . Questi pulsanti aggiungono caratteri a una InputString
proprietà, che viene quindi formattata come numero di telefono per la DisplayText
proprietà . Esiste anche una seconda proprietà di tipo ICommand denominata DeleteCharCommand
. Questa opzione è associata a un pulsante di spaziatura indietro, ma il pulsante deve essere disabilitato se non sono presenti caratteri da eliminare.
L'esempio seguente mostra il codice XAML che usa :KeypadViewModel
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples"
x:Class="XamlSamples.KeypadPage"
Title="Keypad Page">
<ContentPage.BindingContext>
<local:KeypadViewModel />
</ContentPage.BindingContext>
<Grid HorizontalOptions="Center" VerticalOptions="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80" />
<ColumnDefinition Width="80" />
<ColumnDefinition Width="80" />
</Grid.ColumnDefinitions>
<Label Text="{Binding DisplayText}"
Margin="0,0,10,0" FontSize="20" LineBreakMode="HeadTruncation"
VerticalTextAlignment="Center" HorizontalTextAlignment="End"
Grid.ColumnSpan="2" />
<Button Text="⇦" Command="{Binding DeleteCharCommand}" Grid.Column="2"/>
<Button Text="1" Command="{Binding AddCharCommand}" CommandParameter="1" Grid.Row="1" />
<Button Text="2" Command="{Binding AddCharCommand}" CommandParameter="2" Grid.Row="1" Grid.Column="1" />
<Button Text="3" Command="{Binding AddCharCommand}" CommandParameter="3" Grid.Row="1" Grid.Column="2" />
<Button Text="4" Command="{Binding AddCharCommand}" CommandParameter="4" Grid.Row="2" />
<Button Text="5" Command="{Binding AddCharCommand}" CommandParameter="5" Grid.Row="2" Grid.Column="1" />
<Button Text="6" Command="{Binding AddCharCommand}" CommandParameter="6" Grid.Row="2" Grid.Column="2" />
<Button Text="7" Command="{Binding AddCharCommand}" CommandParameter="7" Grid.Row="3" />
<Button Text="8" Command="{Binding AddCharCommand}" CommandParameter="8" Grid.Row="3" Grid.Column="1" />
<Button Text="9" Command="{Binding AddCharCommand}" CommandParameter="9" Grid.Row="3" Grid.Column="2" />
<Button Text="*" Command="{Binding AddCharCommand}" CommandParameter="*" Grid.Row="4" />
<Button Text="0" Command="{Binding AddCharCommand}" CommandParameter="0" Grid.Row="4" Grid.Column="1" />
<Button Text="#" Command="{Binding AddCharCommand}" CommandParameter="#" Grid.Row="4" Grid.Column="2" />
</Grid>
</ContentPage>
In questo esempio la Command
proprietà del primo Button oggetto associato all'oggetto DeleteCharCommand
. Gli altri pulsanti sono associati a AddCharCommand
con un CommandParameter
oggetto che corrisponde al carattere visualizzato in Button: