Bagikan melalui


Pengikatan data dan MVVM

Browse sample. Telusuri sampel

Pola Model-View-ViewModel (MVVM) memberlakukan pemisahan antara tiga lapisan perangkat lunak — antarmuka pengguna XAML, yang disebut tampilan, data yang mendasarinya, yang disebut model, dan perantara antara tampilan dan model, yang disebut viewmodel. Tampilan dan viewmodel sering terhubung melalui pengikatan data yang ditentukan dalam XAML. BindingContext untuk tampilan biasanya adalah instans viewmodel.

Penting

Marshal Aplikasi Multi-platform .NET (.NET MAUI) mengikat pembaruan ke utas UI. Saat menggunakan MVVM, ini memungkinkan Anda memperbarui properti viewmodel terikat data dari utas apa pun, dengan mesin pengikatan .NET MAUI yang membawa pembaruan ke utas UI.

Ada beberapa pendekatan untuk menerapkan pola MVVM, dan artikel ini berfokus pada pendekatan sederhana. Ini menggunakan tampilan dan viewmodel, tetapi bukan model, untuk fokus pada pengikatan data antara dua lapisan. Untuk penjelasan terperinci tentang penggunaan pola MVVM di .NET MAUI, lihat Model-View-ViewModel (MVVM) dalam Pola Aplikasi Perusahaan menggunakan .NET MAUI. Untuk tutorial yang membantu Anda menerapkan pola MVVM, lihat Meningkatkan aplikasi Anda dengan konsep MVVM.

MVVM sederhana

Dalam ekstensi markup XAML, Anda melihat cara menentukan deklarasi namespace XML baru untuk memungkinkan file XAML mereferensikan kelas di rakitan lain. Contoh berikut menggunakan x:Static ekstensi markup untuk mendapatkan tanggal dan waktu saat ini dari properti statis DateTime.Now di System namespace:

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

Dalam contoh ini, nilai yang diambil DateTime ditetapkan sebagai BindingContext pada StackLayout. Ketika Anda mengatur BindingContext pada elemen, elemen tersebut diwarisi oleh semua turunan elemen tersebut. Ini berarti bahwa semua anak memiliki StackLayout yang sama BindingContext, dan mereka dapat berisi pengikatan ke properti objek tersebut:

Screenshot of a page displaying the date and time.

Namun, masalahnya adalah bahwa tanggal dan waktu diatur sekali ketika halaman dibangun dan diinisialisasi, dan tidak pernah berubah.

Halaman XAML dapat menampilkan jam yang selalu menunjukkan waktu saat ini, tetapi memerlukan kode tambahan. Pola MVVM adalah pilihan alami untuk aplikasi MAUI .NET saat pengikatan data dari properti antara objek visual dan data yang mendasarinya. Ketika berpikir dalam hal MVVM, model dan viewmodel adalah kelas yang ditulis sepenuhnya dalam kode. Tampilan sering kali merupakan file XAML yang mereferensikan properti yang ditentukan dalam viewmodel melalui pengikatan data. Dalam MVVM, model tidak tahu viewmodel, dan viewmodel tidak tahu tampilan. Namun, seringkali Anda menyesuaikan jenis yang diekspos oleh viewmodel dengan jenis yang terkait dengan UI.

Catatan

Dalam contoh sederhana MVVM, seperti yang ditunjukkan di sini, seringkali tidak ada model sama sekali, dan pola hanya melibatkan tampilan dan viewmodel yang ditautkan dengan pengikatan data.

Contoh berikut menunjukkan viewmodel untuk jam, dengan satu properti bernama DateTime yang diperbarui setiap detik:

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

Viewmodel biasanya mengimplementasikan INotifyPropertyChanged antarmuka, yang menyediakan kemampuan bagi kelas untuk meningkatkan PropertyChanged peristiwa setiap kali salah satu propertinya berubah. Mekanisme pengikatan data di .NET MAUI melampirkan handler ke kejadian ini PropertyChanged sehingga dapat diberi tahu ketika properti berubah dan menjaga target tetap diperbarui dengan nilai baru. Dalam contoh kode sebelumnya, OnPropertyChanged metode menangani peningkatan peristiwa sambil secara otomatis menentukan nama sumber properti: DateTime.

Contoh berikut menunjukkan XAML yang menggunakan 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>

Dalam contoh ini, ClockViewModel diatur ke BindingContext tag ContentPage elemen properti penggunaan. Atau, file code-behind dapat membuat instans viewmodel.

Ekstensi Binding markup pada Text properti Label dari format DateTime properti . Cuplikan layar berikut menunjukkan hasilnya:

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

Selain itu, dimungkinkan untuk mengakses properti DateTime individual properti viewmodel dengan memisahkan properti dengan titik:

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

MVVM interaktif

MVVM sering digunakan dengan pengikatan data dua arah untuk tampilan interaktif berdasarkan model data yang mendasar.

Contoh berikut menunjukkan HslViewModel yang mengonversi nilai menjadi ColorHue, , Saturationdan Luminosity nilai, dan kembali lagi:

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

Dalam contoh ini, perubahan pada Hueproperti , Saturation, dan Luminosity menyebabkan Color properti berubah, dan perubahan pada Color properti menyebabkan tiga properti lainnya berubah. Ini mungkin tampak seperti perulangan tak terbatas, kecuali bahwa viewmodel tidak memanggil PropertyChanged peristiwa kecuali properti telah berubah.

Contoh XAML berikut berisi properti yang Color terikat ke Color properti viewmodel, dan tiga Slider dan tiga Label tampilan yang terikat ke Hueproperti , Saturation, dan Luminosity :BoxView

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

Pengikatan pada masing-masing Label adalah default OneWay. Ini hanya perlu menampilkan nilai. Namun, pengikatan default pada masing-masing Slider adalah TwoWay. Ini memungkinkan Slider untuk diinisialisasi dari viewmodel. Ketika viewmodel dibuat, properti diatur Color ke Aqua. Perubahan dalam Slider set nilai baru untuk properti di viewmodel, yang kemudian menghitung warna baru:

MVVM using two-way data bindings.

Komandan

Terkadang aplikasi memiliki kebutuhan yang melampaui pengikatan properti dengan mengharuskan pengguna untuk memulai perintah yang memengaruhi sesuatu di viewmodel. Perintah ini umumnya disinyalir oleh klik tombol atau ketukan jari, dan secara tradisional diproses dalam file code-behind dalam handler untuk Clicked peristiwa Button atau Tapped peristiwa TapGestureRecognizer.

Antarmuka perintah menyediakan pendekatan alternatif untuk menerapkan perintah yang jauh lebih cocok untuk arsitektur MVVM. Viewmodel dapat berisi perintah, yang merupakan metode yang dijalankan sebagai reaksi terhadap aktivitas tertentu dalam tampilan seperti Button klik. Pengikatan data didefinisikan antara perintah ini dan Button.

Untuk mengizinkan pengikatan data antara dan Button viewmodel, Button menentukan dua properti:

Catatan

Banyak kontrol lain juga menentukan Command properti dan CommandParameter .

Antarmuka ICommand didefinisikan dalam namespace Layanan System.Windows.Input , dan terdiri dari dua metode dan satu peristiwa:

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

Viewmodel dapat menentukan properti jenis ICommand. Anda kemudian dapat mengikat properti ini ke Command properti masing-masing Button atau elemen lain, atau mungkin tampilan kustom yang mengimplementasikan antarmuka ini. Anda dapat secara opsional mengatur CommandParameter properti untuk mengidentifikasi objek individual Button (atau elemen lain) yang terikat ke properti viewmodel ini. Secara internal, Button memanggil Execute metode setiap kali pengguna mengetuk Button, meneruskan ke Execute metode .CommandParameter

Metode CanExecute dan CanExecuteChanged peristiwa digunakan untuk kasus di mana Button ketukan mungkin saat ini tidak valid, dalam hal ini Button harus menonaktifkan dirinya sendiri. Panggilan ButtonCanExecute saat Command properti pertama kali diatur dan setiap kali CanExecuteChanged peristiwa dinaikkan. Jika CanExecute mengembalikan false, menonaktifkan Button dirinya sendiri dan tidak menghasilkan Execute panggilan.

Anda dapat menggunakan kelas atau Command<T> yang Command disertakan dalam .NET MAUI untuk mengimplementasikan ICommand antarmuka. Kedua kelas ini menentukan beberapa konstruktor ditambah ChangeCanExecute metode yang dapat dipanggil viewmodel untuk memaksa Command objek menaikkan CanExecuteChanged peristiwa.

Contoh berikut menunjukkan viewmodel untuk keypad sederhana yang dimaksudkan untuk memasukkan nomor telepon:

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

Dalam contoh ini, Execute metode dan CanExecute untuk perintah didefinisikan sebagai fungsi lambda dalam konstruktor. Viewmodel mengasumsikan bahwa AddCharCommand properti terikat ke Command properti beberapa tombol (atau kontrol lain yang memiliki antarmuka perintah), yang masing-masing diidentifikasi oleh CommandParameter. Tombol-tombol ini menambahkan karakter ke InputString properti, yang kemudian diformat sebagai nomor telepon untuk DisplayText properti . Ada juga properti kedua dari jenis ICommand bernama DeleteCharCommand. Ini terikat ke tombol penspasian belakang, tetapi tombol harus dinonaktifkan jika tidak ada karakter yang akan dihapus.

Contoh berikut menunjukkan XAML yang menggunakan 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>

Dalam contoh ini, Command properti pertama Button yang terikat ke DeleteCharCommand. Tombol lain terikat dengan AddCharCommand dengan yang sama dengan CommandParameter karakter yang muncul di Button:

Screenshot of a calculator using MVVM and commands.