Condividi tramite


Data binding e MVVM

Browse sample. Esplorare l'esempio

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 BindingContexte possono contenere associazioni alle proprietà di tale oggetto:

Screenshot of a page displaying the date and time.

Tuttavia, il problema è che la data e l'ora vengono impostate una volta quando la pagina viene costruita e inizializzata e non cambia mai.

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:

Screenshot of a page displaying the date and time via a viewmodel.

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 Huevalori , Saturatione 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 Hueproprietà , Saturatione 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 SliderLabel visualizzazioni associate alle Hueproprietà , Saturatione 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:

MVVM using two-way data bindings.

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à:

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 ButtonCanExecute 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 CommandParameterdi 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="&#x21E6;" 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:

Screenshot of a calculator using MVVM and commands.