Parte 5. Da data binding a MVVM
Il modello architetturale Model-View-ViewModel (MVVM) è stato inventato tenendo presente XAML. Il modello applica una separazione tra tre livelli software, ovvero l'interfaccia utente XAML, denominata View; i dati sottostanti, denominati Modello; e un intermediario tra View e Model, denominato ViewModel. View e ViewModel sono spesso connessi tramite data binding definiti nel file XAML. BindingContext per view è in genere un'istanza di ViewModel.
Modello di visualizzazione semplice
Come introduzione a ViewModels, esaminiamo prima un programma senza uno.
In precedenza è 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. Ecco un programma che definisce una dichiarazione dello spazio dei nomi XML per lo spazio dei System
nomi:
xmlns:sys="clr-namespace:System;assembly=netstandard"
Il programma può usare x:Static
per ottenere la data e l'ora correnti dalla proprietà statica DateTime.Now
e impostare tale DateTime
valore su su BindingContext
in un StackLayout
oggetto :
<StackLayout BindingContext="{x:Static sys:DateTime.Now}" …>
BindingContext
è una proprietà speciale: 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
oggetto e possono contenere associazioni semplici alle proprietà di tale oggetto.
Nel programma One-Shot DateTime, due degli elementi figlio contengono associazioni alle proprietà di tale DateTime
valore, ma altri due elementi figlio contengono associazioni che sembrano mancare un percorso di associazione. Ciò significa che il DateTime
valore stesso viene usato per :StringFormat
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
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">
<StackLayout BindingContext="{x:Static sys:DateTime.Now}"
HorizontalOptions="Center"
VerticalOptions="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}'}" />
</StackLayout>
</ContentPage>
Il problema è che la data e l'ora vengono impostate una volta alla prima compilazione della pagina e non cambiano mai:
Un file XAML può visualizzare un orologio che mostra sempre l'ora corrente, ma richiede codice per aiutare. Quando si pensa in termini di MVVM, Model e ViewModel sono classi scritte interamente nel codice. La visualizzazione è spesso un file XAML che fa riferimento alle proprietà definite in ViewModel tramite data binding.
Un modello appropriato è ignorante di ViewModel e un ViewModel appropriato è ignorante della visualizzazione. Tuttavia, spesso un programmatore adatta i tipi di dati esposti da ViewModel ai tipi di dati associati a interfacce utente specifiche. Ad esempio, se un modello accede a un database che contiene stringhe ASCII a 8 bit, ViewModel dovrà eseguire la conversione tra tali stringhe in stringhe Unicode per supportare l'uso esclusivo di Unicode nell'interfaccia utente.
In semplici esempi di MVVM (ad esempio quelli mostrati qui), spesso non esiste alcun modello e il modello implica solo un view e ViewModel collegato con i data binding.
Ecco un viewModel per un orologio con una sola proprietà denominata DateTime
, che aggiorna tale DateTime
proprietà ogni secondo:
using System;
using System.ComponentModel;
using Xamarin.Forms;
namespace XamlSamples
{
class ClockViewModel : INotifyPropertyChanged
{
DateTime dateTime;
public event PropertyChangedEventHandler PropertyChanged;
public ClockViewModel()
{
this.DateTime = DateTime.Now;
Device.StartTimer(TimeSpan.FromSeconds(1), () =>
{
this.DateTime = DateTime.Now;
return true;
});
}
public DateTime DateTime
{
set
{
if (dateTime != value)
{
dateTime = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("DateTime"));
}
}
}
get
{
return dateTime;
}
}
}
}
ViewModels implementa in genere l'interfaccia INotifyPropertyChanged
, il che significa che la classe genera un PropertyChanged
evento ogni volta che una delle relative proprietà cambia. Il meccanismo di data binding in Xamarin.Forms associa 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.
Un orologio basato su questo ViewModel può essere semplice come segue:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
x:Class="XamlSamples.ClockPage"
Title="Clock Page">
<Label Text="{Binding DateTime, StringFormat='{0:T}'}"
FontSize="Large"
HorizontalOptions="Center"
VerticalOptions="Center">
<Label.BindingContext>
<local:ClockViewModel />
</Label.BindingContext>
</Label>
</ContentPage>
Si noti che l'oggetto ClockViewModel
è impostato su BindingContext
dell'oggetto dei tag dell'elemento Label
della proprietà . In alternativa, è possibile creare un'istanza di ClockViewModel
in una Resources
raccolta e impostarla su tramite un'estensione BindingContext
StaticResource
di markup. In alternativa, il file code-behind può creare un'istanza di ViewModel.
Estensione Binding
di markup nella Text
proprietà della Label
proprietà formatta la DateTime
proprietà . Ecco la visualizzazione:
È anche possibile accedere alle singole proprietà della DateTime
proprietà di ViewModel 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.
Ecco una classe denominata HslViewModel
che converte un Color
valore in Hue
valori , Saturation
e Luminosity
e e viceversa:
using System;
using System.ComponentModel;
using Xamarin.Forms;
namespace XamlSamples
{
public class HslViewModel : INotifyPropertyChanged
{
double hue, saturation, luminosity;
Color color;
public event PropertyChangedEventHandler PropertyChanged;
public double Hue
{
set
{
if (hue != value)
{
hue = value;
OnPropertyChanged("Hue");
SetNewColor();
}
}
get
{
return hue;
}
}
public double Saturation
{
set
{
if (saturation != value)
{
saturation = value;
OnPropertyChanged("Saturation");
SetNewColor();
}
}
get
{
return saturation;
}
}
public double Luminosity
{
set
{
if (luminosity != value)
{
luminosity = value;
OnPropertyChanged("Luminosity");
SetNewColor();
}
}
get
{
return luminosity;
}
}
public Color Color
{
set
{
if (color != value)
{
color = value;
OnPropertyChanged("Color");
Hue = value.Hue;
Saturation = value.Saturation;
Luminosity = value.Luminosity;
}
}
get
{
return color;
}
}
void SetNewColor()
{
Color = Color.FromHsla(Hue, Saturation, Luminosity);
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Le modifiche apportate alle Hue
proprietà , Saturation
e Luminosity
causano la modifica della Color
proprietà e le modifiche Color
causano la modifica delle altre tre proprietà. Questo potrebbe sembrare un ciclo infinito, ad eccezione del fatto che la classe non richiama l'evento PropertyChanged
a meno che la proprietà non sia stata modificata. In questo modo viene risolto un ciclo di feedback non controllabile.
Il file XAML seguente contiene una BoxView
la cui Color
proprietà è associata alla Color
proprietà di ViewModel e tre e tre Slider
Label
visualizzazioni associate alle Hue
proprietà , Saturation
e Luminosity
:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
x:Class="XamlSamples.HslColorScrollPage"
Title="HSL Color Scroll Page">
<ContentPage.BindingContext>
<local:HslViewModel Color="Aqua" />
</ContentPage.BindingContext>
<StackLayout Padding="10, 0">
<BoxView Color="{Binding Color}"
VerticalOptions="FillAndExpand" />
<Label Text="{Binding Hue, StringFormat='Hue = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Hue, Mode=TwoWay}" />
<Label Text="{Binding Saturation, StringFormat='Saturation = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Saturation, Mode=TwoWay}" />
<Label Text="{Binding Luminosity, StringFormat='Luminosity = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Luminosity, Mode=TwoWay}" />
</StackLayout>
</ContentPage>
L'associazione per ogni Label
è l'oggetto predefinito OneWay
. Deve solo visualizzare il valore. Ma l'associazione su ogni Slider
oggetto è TwoWay
. In questo modo l'oggetto Slider
può essere inizializzato da ViewModel. Si noti che la Color
proprietà è impostata su Aqua
quando viene creata un'istanza di ViewModel. Tuttavia, una modifica nell'oggetto Slider
deve anche impostare un nuovo valore per la proprietà in ViewModel, che quindi calcola un nuovo colore.
Esecuzione di comandi con ViewModels
In molti casi, il modello MVVM è limitato alla manipolazione degli elementi di dati: oggetti dell'interfaccia utente negli oggetti dati paralleli view in ViewModel.
Tuttavia, a volte la visualizzazione deve contenere pulsanti che attivano varie azioni in ViewModel. Tuttavia, ViewModel non deve contenere Clicked
gestori per i pulsanti perché collega viewModel a un paradigma specifico dell'interfaccia utente.
Per consentire a ViewModel di essere più indipendenti da specifici oggetti dell'interfaccia utente, ma consentire comunque la chiamata dei metodi all'interno di ViewModel, esiste un'interfaccia di comando . Questa interfaccia di comando è supportata dagli elementi seguenti in Xamarin.Forms:
Button
MenuItem
ToolbarItem
SearchBar
TextCell
(e quindi ancheImageCell
)ListView
TapGestureRecognizer
Ad eccezione dell'elemento SearchBar
e ListView
, questi elementi definiscono due proprietà:
Command
di tipoSystem.Windows.Input.ICommand
CommandParameter
di tipoObject
Definisce SearchBar
e proprietà, mentre definisce ListView
una RefreshCommand
proprietà di tipo ICommand
.SearchCommandParameter
SearchCommand
L'interfaccia ICommand
definisce due metodi e un evento:
void Execute(object arg)
bool CanExecute(object arg)
event EventHandler CanExecuteChanged
ViewModel può definire 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 , Button
passando 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.
Per informazioni sull'aggiunta di comandi a ViewModels, Xamarin.Forms definisce due classi che implementano ICommand
: Command
e Command<T>
dove T
è il tipo degli argomenti in Execute
e CanExecute
. Queste due classi definiscono diversi costruttori e un ChangeCanExecute
metodo che viewModel può chiamare per forzare l'evento Command
CanExecuteChanged
.
Ecco un viewModel per un tastierino semplice destinato all'immissione di numeri di telefono. Si noti che il Execute
metodo e CanExecute
sono definiti come funzioni lambda direttamente nel costruttore:
using System;
using System.ComponentModel;
using System.Windows.Input;
using Xamarin.Forms;
namespace XamlSamples
{
class KeypadViewModel : INotifyPropertyChanged
{
string inputString = "";
string displayText = "";
char[] specialChars = { '*', '#' };
public event PropertyChangedEventHandler PropertyChanged;
// Constructor
public KeypadViewModel()
{
AddCharCommand = new Command<string>((key) =>
{
// Add the key to the input string.
InputString += key;
});
DeleteCharCommand = new Command(() =>
{
// Strip a character from the input string.
InputString = InputString.Substring(0, InputString.Length - 1);
},
() =>
{
// Return true if there's something to delete.
return InputString.Length > 0;
});
}
// Public properties
public string InputString
{
protected set
{
if (inputString != value)
{
inputString = value;
OnPropertyChanged("InputString");
DisplayText = FormatText(inputString);
// Perhaps the delete button must be enabled/disabled.
((Command)DeleteCharCommand).ChangeCanExecute();
}
}
get { return inputString; }
}
public string DisplayText
{
protected set
{
if (displayText != value)
{
displayText = value;
OnPropertyChanged("DisplayText");
}
}
get { return displayText; }
}
// ICommand implementations
public ICommand AddCharCommand { protected set; get; }
public ICommand DeleteCharCommand { protected set; get; }
string FormatText(string str)
{
bool hasNonNumbers = str.IndexOfAny(specialChars) != -1;
string formatted = str;
if (hasNonNumbers || str.Length < 4 || str.Length > 10)
{
}
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;
}
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Questo ViewModel presuppone che la AddCharCommand
proprietà sia associata alla Command
proprietà di più pulsanti (o qualsiasi altro elemento con un'interfaccia CommandParameter
di comando), ognuno dei quali è identificato 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.
Il tastierino seguente non è così sofisticato visivamente come potrebbe essere. Al contrario, il markup è stato ridotto al minimo per illustrare più chiaramente l'uso dell'interfaccia del comando:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
x:Class="XamlSamples.KeypadPage"
Title="Keypad Page">
<Grid HorizontalOptions="Center"
VerticalOptions="Center">
<Grid.BindingContext>
<local:KeypadViewModel />
</Grid.BindingContext>
<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>
<!-- Internal Grid for top row of items -->
<Grid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Frame Grid.Column="0"
OutlineColor="Accent">
<Label Text="{Binding DisplayText}" />
</Frame>
<Button Text="⇦"
Command="{Binding DeleteCharCommand}"
Grid.Column="1"
BorderWidth="0" />
</Grid>
<Button Text="1"
Command="{Binding AddCharCommand}"
CommandParameter="1"
Grid.Row="1" Grid.Column="0" />
<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" Grid.Column="0" />
<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" Grid.Column="0" />
<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" Grid.Column="0" />
<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>
La Command
proprietà del primo Button
oggetto visualizzato in questo markup è associata a DeleteCharCommand
. Il resto è associato a con un CommandParameter
oggetto che corrisponde al AddCharCommand
carattere visualizzato sul Button
viso. Ecco il programma in azione:
Richiamo di metodi asincroni
I comandi possono anche richiamare metodi asincroni. A tale scopo, usare le async
parole chiave e await
quando si specifica il Execute
metodo :
DownloadCommand = new Command (async () => await DownloadAsync ());
Ciò indica che il DownloadAsync
metodo è un Task
oggetto e deve essere atteso:
async Task DownloadAsync ()
{
await Task.Run (() => Download ());
}
void Download ()
{
...
}
Implementazione di un menu di spostamento
Il programma di esempio che contiene tutto il codice sorgente di questa serie di articoli usa viewModel per la home page. Questo ViewModel è una definizione di una classe breve con tre proprietà denominate Type
, Title
e Description
che contengono il tipo di ognuna delle pagine di esempio, un titolo e una breve descrizione. Inoltre, ViewModel definisce una proprietà statica denominata All
che è un insieme di tutte le pagine del programma:
public class PageDataViewModel
{
public PageDataViewModel(Type type, string title, string description)
{
Type = type;
Title = title;
Description = description;
}
public Type Type { private set; get; }
public string Title { private set; get; }
public string Description { private set; get; }
static PageDataViewModel()
{
All = new List<PageDataViewModel>
{
// Part 1. Getting Started with XAML
new PageDataViewModel(typeof(HelloXamlPage), "Hello, XAML",
"Display a Label with many properties set"),
new PageDataViewModel(typeof(XamlPlusCodePage), "XAML + Code",
"Interact with a Slider and Button"),
// Part 2. Essential XAML Syntax
new PageDataViewModel(typeof(GridDemoPage), "Grid Demo",
"Explore XAML syntax with the Grid"),
new PageDataViewModel(typeof(AbsoluteDemoPage), "Absolute Demo",
"Explore XAML syntax with AbsoluteLayout"),
// Part 3. XAML Markup Extensions
new PageDataViewModel(typeof(SharedResourcesPage), "Shared Resources",
"Using resource dictionaries to share resources"),
new PageDataViewModel(typeof(StaticConstantsPage), "Static Constants",
"Using the x:Static markup extensions"),
new PageDataViewModel(typeof(RelativeLayoutPage), "Relative Layout",
"Explore XAML markup extensions"),
// Part 4. Data Binding Basics
new PageDataViewModel(typeof(SliderBindingsPage), "Slider Bindings",
"Bind properties of two views on the page"),
new PageDataViewModel(typeof(SliderTransformsPage), "Slider Transforms",
"Use Sliders with reverse bindings"),
new PageDataViewModel(typeof(ListViewDemoPage), "ListView Demo",
"Use a ListView with data bindings"),
// Part 5. From Data Bindings to MVVM
new PageDataViewModel(typeof(OneShotDateTimePage), "One-Shot DateTime",
"Obtain the current DateTime and display it"),
new PageDataViewModel(typeof(ClockPage), "Clock",
"Dynamically display the current time"),
new PageDataViewModel(typeof(HslColorScrollPage), "HSL Color Scroll",
"Use a view model to select HSL colors"),
new PageDataViewModel(typeof(KeypadPage), "Keypad",
"Use a view model for numeric keypad logic")
};
}
public static IList<PageDataViewModel> All { private set; get; }
}
Il file XAML per MainPage
definisce una ListBox
la cui ItemsSource
proprietà è impostata su tale All
proprietà e che contiene un TextCell
oggetto per la visualizzazione delle Title
proprietà e Description
di ogni pagina:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples"
x:Class="XamlSamples.MainPage"
Padding="5, 0"
Title="XAML Samples">
<ListView ItemsSource="{x:Static local:PageDataViewModel.All}"
ItemSelected="OnListViewItemSelected">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Title}"
Detail="{Binding Description}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage>
Le pagine vengono visualizzate in un elenco scorrevole:
Il gestore nel file code-behind viene attivato quando l'utente seleziona un elemento. Il gestore imposta la SelectedItem
proprietà della proprietà di ListBox
nuovo su null
e quindi crea un'istanza della pagina selezionata e lo passa:
private async void OnListViewItemSelected(object sender, SelectedItemChangedEventArgs args)
{
(sender as ListView).SelectedItem = null;
if (args.SelectedItem != null)
{
PageDataViewModel pageData = args.SelectedItem as PageDataViewModel;
Page page = (Page)Activator.CreateInstance(pageData.Type);
await Navigation.PushAsync(page);
}
}
Video
Xamarin Evolve 2016: MVVM reso semplice con Xamarin.Forms e prism
Riepilogo
XAML è uno strumento potente per definire le interfacce utente nelle Xamarin.Forms applicazioni, in particolare quando vengono usati data binding e MVVM. Il risultato è una rappresentazione pulita, elegante e potenzialmente utilizzabile di un'interfaccia utente con tutto il supporto in background nel codice.
Collegamenti correlati
- Parte 1. Introduzione a XAML
- Part 2 (Distribuire macchine virtuali nel cloud: Parte 2). Sintassi XAML essenziale
- Parte 3. Estensioni di markup XAML
- Parte 4. Nozioni di base sul data binding
Video correlati
Altri video di Xamarin sono disponibili su Channel 9 e YouTube.