Dela via


Databindning och MVVM

Bläddra bland exempel. Bläddra i exemplet

MVVM-mönstret (Model-View-ViewModel) framtvingar en separation mellan tre programvarulager – XAML-användargränssnittet, kallat vyn, underliggande data, som kallas modellen, och en mellanhand mellan vyn och modellen, som kallas viewmodel. Vyn och viewmodel är ofta anslutna via databindningar som definieras i XAML. BindingContext för vyn är vanligtvis en instans av vymodellen.

Viktigt!

.NET Multi-platform App UI (.NET MAUI) konverterar bindningsuppdateringar till användargränssnittstråden. När du använder MVVM kan du uppdatera databundna viewmodel-egenskaper från valfri tråd, med .NET MAUI:s bindningsmotor som ger uppdateringarna till användargränssnittstråden.

Det finns flera metoder för att implementera MVVM-mönstret, och den här artikeln fokuserar på en enkel metod. Den använder vyer och visningsmodeller, men inte modeller, för att fokusera på databindningen mellan de två lagren. En detaljerad förklaring av hur du använder MVVM-mönstret i .NET MAUI finns i Modell-View-ViewModel (MVVM) i Enterprise Application Patterns using .NET MAUI. En självstudiekurs som hjälper dig att implementera MVVM-mönstret finns i Uppgradera din app med MVVM-begrepp.

Enkel MVVM

I XAML-markeringstillägg såg du hur du definierar en ny XML-namnområdesdeklaration så att en XAML-fil kan referera till klasser i andra sammansättningar. I följande exempel används markeringstillägget x:Static för att hämta aktuellt datum och tid från den statiska DateTime.Now egenskapen i System namnområdet:

<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"
             x:DataType="sys:DateTime">

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

I det här exemplet anges det hämtade DateTime-värdet som BindingContext på en StackLayout. När du anger BindingContext för ett element ärvs det av alla underordnade element i det elementet. Det innebär att alla underordnade StackLayout objekt har samma BindingContext, och de kan innehålla bindningar till objektets egenskaper:

Skärmbild av en sida som visar datum och tid.

Problemet är dock att datum och tid anges en gång när sidan har konstruerats och initierats och aldrig ändras.

Varning

I en klass som härleds från BindableObjectkan endast egenskaper av typen BindableProperty bindas. Till exempel VisualElement.IsLoaded och Element.Parent kan inte bindas.

En XAML-sida kan visa en klocka som alltid visar den aktuella tiden, men den kräver ytterligare kod. MVVM-mönstret är ett naturligt val för .NET MAUI-appar när data binds från egenskaper mellan visuella objekt och underliggande data. När du tänker i termer av MVVM är modellen och viewmodel klasser skrivna helt i kod. Vyn är ofta en XAML-fil som refererar till egenskaper som definierats i viewmodel via databindningar. I MVVM är en modell okunnig om viewmodel och en viewmodel är okunnig om vyn. Men ofta skräddarsyr du de typer som exponeras av viewmodel till de typer som är associerade med användargränssnittet.

Anmärkning

I enkla exempel på MVVM, till exempel de som visas här, finns det ofta ingen modell alls, och mönstret omfattar bara en vy och viewmodel som är länkad med databindningar.

I följande exempel visas en vymodell för en klocka med en enda egenskap med namnet DateTime som uppdateras varje sekund:

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));
}

Viewmodels implementerar vanligtvis INotifyPropertyChanged-gränssnittet, vilket ger en klass möjlighet att utlösa PropertyChanged-händelsen så snart en av dess egenskaper ändras. Databindningsmekanismen i .NET MAUI kopplar en hanterare till den här PropertyChanged händelsen så att den kan meddelas när en egenskap ändras och hålla målet uppdaterat med det nya värdet. I föregående kodexempel OnPropertyChanged hanterar metoden att höja händelsen samtidigt som egenskapens källnamn automatiskt fastställs: DateTime.

I följande exempel visas XAML som använder 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"
             x:DataType="local:ClockViewModel">
    <ContentPage.BindingContext>
        <local:ClockViewModel />
    </ContentPage.BindingContext>

    <Label Text="{Binding DateTime, StringFormat='{0:T}'}"
           FontSize="18"
           HorizontalOptions="Center"
           VerticalOptions="Center" />
</ContentPage>

I det här exemplet är ClockViewModel inställt till BindingContext av ContentPage med hjälp av egenskapsblocktaggar. Alternativt kan filen bakom koden instansiera viewmodel.

Binding-markeringstillägget på egenskapen Text formaterar DateTime-egenskapen av Label. Följande skärmbild visar resultatet:

Skärmbild av en sida som visar datum och tid via en visningsmodell.

Dessutom är det möjligt att få tillgång till enskilda egenskaper hos viewmodelens DateTime-egenskap genom att avgränsa egenskaperna med punkter:

<Label Text="{Binding DateTime.Second, StringFormat='{0}'}" … >

Interaktiv MVVM

MVVM används ofta med dubbelriktade databindningar för en interaktiv vy baserat på en underliggande datamodell.

I följande exempel visas HslViewModel som konverterar ett Color värde till Hue, Saturationoch Luminosity värden och tillbaka igen:

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));
}

I det här exemplet leder ändringar i egenskaperna Hue, Saturation och Luminosity till att egenskapen Color ändras, och ändringar i egenskapen Color leder till att de övriga tre egenskaperna ändras. Detta kan verka som en oändlig loop, förutom att viewmodel inte anropar PropertyChanged händelsen om inte egenskapen har ändrats.

Följande XAML-exempel innehåller en BoxView vars Color-egenskap är kopplad till Color-egenskapen i viewmodel, och tre Slider och tre Label-vyer som är kopplade till Hue-, Saturation- och Luminosity-egenskaperna.

<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"
             x:DataType="local:HslViewModel">
    <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>

Bindningen för var och en Label är standard OneWay. Det behöver bara visa värdet. Standardbindningen för var och en Slider är TwoWaydock . På så sätt kan Slider initieras från viewmodel. När viewmodel instansieras är egenskapen Color inställd på Aqua. En ändring i en Slider anger ett nytt värde för egenskapen i viewmodel, som sedan beräknar en ny färg:

MVVM med hjälp av tvåvägs-databindningar.

Bestämmande

Ibland har en app behov som går utöver egenskapsbindningar genom att kräva att användaren initierar kommandon som påverkar något i viewmodel. Dessa kommandon signaleras vanligtvis med knappklick eller fingertryckningar, och traditionellt bearbetas de i kodens bakgrundsfil i en hanterare för Clicked-händelsen eller ButtonTapped-händelsen för en TapGestureRecognizer.

Kommandogränssnittet ger en alternativ metod för att implementera kommandon som är mycket bättre lämpade för MVVM-arkitekturen. Viewmodel kan innehålla kommandon, som är metoder som körs som en reaktion på en specifik aktivitet i vyn, till exempel ett Button klick. Databindningar definieras mellan dessa kommandon och Button.

För att tillåta en databindning mellan en Button och en viewmodel definierar de Button två egenskaperna:

Anmärkning

Många andra kontroller definierar Command och CommandParameter egenskaper.

Gränssnittet ICommand definieras i namnområdet System.Windows.Input och består av två metoder och en händelse:

  • void Execute(object arg)
  • bool CanExecute(object arg)
  • event EventHandler CanExecuteChanged

Viewmodel kan definiera egenskaper av typen ICommand. Du kan sedan binda dessa egenskaper till egenskapen för Command varje Button eller annat element, eller kanske en anpassad vy som implementerar det här gränssnittet. Du kan också ange CommandParameter egenskapen för att identifiera enskilda Button objekt (eller andra element) som är bundna till den här viewmodel-egenskapen. Internt anropar Button metoden Execute när användaren trycker på Button, och skickar dess CommandParameter till Execute metoden.

Metoden CanExecute och händelsen CanExecuteChanged används i fall där en Button tryckning för närvarande kan vara ogiltig, och i så fall bör Button inaktivera sig själv. Anropar ButtonCanExecute när egenskapen Command initialt sätts och när händelsen CanExecuteChanged utlöses. Om CanExecute returnerar false, inaktiverar Button sig själv och genererar inte Execute anrop.

Du kan använda Command klassen eller Command<T> som ingår i .NET MAUI för att implementera ICommand gränssnittet. Dessa två klasser definierar flera konstruktorer plus en ChangeCanExecute metod som viewmodel kan anropa för att tvinga Command objektet att skapa CanExecuteChanged händelsen.

I följande exempel visas en vymodell för en enkel knappsats som är avsedd för att ange telefonnummer:

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));
}

I det här exemplet definieras Execute- och CanExecute-metoderna för kommandona som lambda-funktioner i konstruktorn. Viewmodellen förutsätter att AddCharCommand-egenskapen är bunden till Command-egenskapen hos flera knappar (eller andra kontroller som har ett kommandogränssnitt), som var och en identifieras av CommandParameter. De här knapparna lägger till tecken i en InputString egenskap som sedan formateras som ett telefonnummer för egenskapen DisplayText . Det finns också en andra egenskap av typen ICommand med namnet DeleteCharCommand. Detta är bundet till en bakåtavståndsknapp, men knappen bör inaktiveras om det inte finns några tecken att ta bort.

I följande exempel visas XAML som använder 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"
             x:DataType="local:KeypadViewModel">
    <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>

I det här exemplet är egenskapen Command för den första Button, som är bunden till DeleteCharCommand. De andra knapparna är bundna till AddCharCommand med ett CommandParameter som är samma som tecknet som visas på Button:

Skärmbild av en kalkylator med hjälp av MVVM och kommandon.