Pengikatan data dan MVVM
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:
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:
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 Color Hue
, , Saturation
dan 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 Hue
properti , 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 Hue
properti , 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:
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:
Command
dari jenisSystem.Windows.Input.ICommand
CommandParameter
dari jenisObject
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 Button CanExecute
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="⇦" 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: