資料繫結和 MVVM
Model-View-ViewModel (MVVM) 模式會強制執行三個軟體層之間的分隔:XAML 使用者介面,稱為檢視、基礎數據、稱為模型,以及檢視與模型之間的媒介,稱為 viewmodel。 檢視和 viewmodel 通常會透過 XAML 中定義的數據系結來連接。 BindingContext
檢視的 通常是 viewmodel 的實例。
重要
.NET 多平臺應用程式 UI (.NET MAUI) 會將系結更新封送處理至 UI 線程。 使用MVVM時,這可讓您從任何線程更新數據系結 ViewModel 屬性,並使用 .NET MAUI 的系結引擎將更新帶入 UI 線程。
實作MVVM模式的方法有很多種,本文著重於簡單的方法。 它會使用檢視和 viewmodel,但不是模型,將焦點放在兩個層之間的數據系結。 如需在 .NET MAUI 中使用MVVM模式的詳細說明,請參閱使用 .NET MAUI 的企業應用程式模式中的Model-View-ViewModel (MVVM)。 如需可協助您實作MVVM模式的教學課程,請參閱 使用MVVM概念升級您的應用程式。
簡單MVVM
在 XAML 標記延伸中,您已瞭解如何定義新的 XML 命名空間宣告,以允許 XAML 檔案參考其他元件中的類別。 下列範例會x:Static
使用標記延伸,從 命名空間中的System
靜態DateTime.Now
屬性取得目前的日期和時間:
<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>
在這裡範例中,擷取 DateTime
的值會設定為 BindingContext
上的 StackLayout。 當您在項目上設定 BindingContext
時,它會由該專案的所有子系繼承。 這表示 的所有子系 StackLayout 都有相同的 BindingContext
,而且它們可以包含該對象的屬性系結:
不過,問題是當頁面建構和初始化時,日期和時間會設定一次,而且永遠不會變更。
警告
在衍生自 BindableObject的類別中,只有類型的 BindableProperty 屬性是可系結的。 例如, VisualElement.IsLoaded 和 Element.Parent 不可繫結。
XAML 頁面可以顯示一律顯示目前時間的時鐘,但需要額外的程式代碼。 MVVM 模式是 .NET MAUI 應用程式在視覺對象與基礎數據之間從屬性系結數據時的自然選擇。 在MVVM方面思考時,模型和 viewmodel 是完全以程式代碼撰寫的類別。 檢視通常是一個 XAML 檔案,可透過數據系結參考 ViewModel 中定義的屬性。 在MVVM中,模型會忽略 viewmodel,而 viewmodel 則忽略檢視。 不過,您通常會針對與UI相關聯的類型,量身打造 viewmodel 所公開的類型。
注意
在MVVM的簡單範例中,例如這裡所示的範例,通常完全沒有模型,而且模式只牽涉到與數據系結連結的檢視和 ViewModel。
下列範例顯示時鐘的 viewmodel,其單一屬性名為 DateTime
,每秒更新一次:
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 通常會實 INotifyPropertyChanged
作 介面,只要其中一個屬性變更,類別就能夠引發 PropertyChanged
事件。 .NET MAUI 中的數據系結機制會將處理程式附加至此 PropertyChanged
事件,以便在屬性變更時收到通知,並讓目標保持以新值更新。 在上述程式代碼範例中 OnPropertyChanged
,方法會在自動判斷屬性來源名稱時處理引發事件: DateTime
。
下列範例顯示取用 的 ClockViewModel
XAML:
<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>
在這裡範例中,會 ClockViewModel
設定為 BindingContext
using 屬性項目標記的 ContentPage 。 或者,程序代碼後置檔案可以具現化 viewmodel。
Binding
屬性上的Text
Label標記延伸會格式化 DateTime
屬性。 下列螢幕快照顯示結果:
此外,藉由使用句點分隔屬性,即可存取 viewmodel 屬性的個別屬性 DateTime
:
<Label Text="{Binding DateTime.Second, StringFormat='{0}'}" … >
互動式MVVM
MVVM 通常會與雙向數據系結搭配使用,以根據基礎數據模型進行互動式檢視。
下列範例顯示 HslViewModel
,將值轉換成 Color Hue
、 Saturation
和 Luminosity
值,然後再次返回:
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));
}
在此範例中,、 和 Luminosity
屬性的Saturation
Hue
變更會導致 Color
屬性變更,而屬性的變更Color
會導致其他三個屬性變更。 這似乎是無限迴圈,除非屬性已變更,否則 viewmodel 不會叫用 PropertyChanged
事件。
下列 XAML 範例包含 BoxView ,其 Color
屬性系結至 viewmodel 的 屬性,以及三個Slider和三Label個系結至 Color
、 Saturation
和 Luminosity
屬性的Hue
檢視:
<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>
每個 Label 上的系結都是預設 OneWay
的 。 它只需要顯示值。 不過,每個 Slider 上的預設系結是 TwoWay
。 這可讓 Slider 從 viewmodel 初始化 。 當 viewmodel 具現化時,它的 Color
屬性會設定為 Aqua
。 中的 Slider 變更會設定 viewmodel 中屬性的新值,然後計算新的色彩:
命令
有時候應用程式需要超出屬性系結的需求,方法是要求使用者起始會影響 viewmodel 中某些事物的命令。 這些命令通常是透過按鈕點擊或手指點選發出訊號,傳統上會以 Button 的 Clicked
事件處理常式或 TapGestureRecognizer 的 Tapped
事件處理常式在程式碼後置檔案中加以處理。
命令介面可針對比較適合 MVVM 架構的命令實作,提供一種替代方法。 viewmodel 可以包含命令,這些命令是響應檢視中特定活動的方法,例如 Button 按兩下。 資料繫結會定義在這些命令與 Button 之間。
若要允許 與 viewmodel 之間的 Button 數據系結,定義 Button 兩個屬性:
- 型別
Command
的System.Windows.Input.ICommand
- 型別
CommandParameter
的Object
注意
許多其他控制件也會定義 Command
和 CommandParameter
屬性。
介面 ICommand 定義於 System.Windows.Input 命名空間中,由兩個方法和一個事件所組成:
void Execute(object arg)
bool CanExecute(object arg)
event EventHandler CanExecuteChanged
viewmodel 可以定義 類型的 ICommand屬性。 然後,您可以將這些屬性系結至 Command
彼此 Button 或其他項目的 屬性,或是實作這個介面的自定義檢視。 您可以選擇性地設定 屬性, CommandParameter
以識別系結至這個 viewmodel 屬性的個別 Button 物件(或其他專案)。 在內部,每當用戶點選 Button時,就會Button呼叫 Execute
方法,並傳遞至 Execute
方法。CommandParameter
方法 CanExecute
與 CanExecuteChanged
事件用於 Button 點選目前可能無效的情況,在此情況下 Button ,應該停用本身。 當Button屬性第一次設定,以及每當引發事件時,就會CanExecuteChanged
呼叫 CanExecute
Command
。 如果 CanExecute
傳 false
回 ,則會 Button 停用本身,而且不會產生 Execute
呼叫。
您可以使用 Command
.NET MAUI 中包含的 或 Command<T>
類別來實作 ICommand 介面。 這兩個類別會定義數個 ChangeCanExecute
建構函式,再加上 viewmodel 可以呼叫的方法,以強制 Command
對象引發 CanExecuteChanged
事件。
下列範例顯示用於輸入電話號碼之簡單鍵盤的 viewmodel:
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));
}
在此範例中, Execute
命令的 和 CanExecute
方法會定義為建構函式中的 Lambda 函式。 viewmodel 假設 AddCharCommand
屬性系結至 Command
數個按鈕的 屬性(或具有命令介面的任何其他控件),每個控件都是由 CommandParameter
所識別。 這些按鈕會將字元新增至 InputString
屬性,然後格式化為 屬性的 DisplayText
電話號碼。 另外還有名為DeleteCharCommand
類型的ICommand第二個屬性。 這會系結至返回間距按鈕,但如果沒有要刪除的字元,則應該停用該按鈕。
下列範例顯示取用 的 KeypadViewModel
XAML:
<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>
在此範例中,Command
系結至 DeleteCharCommand
的第一個 Button 屬性。 其他按鈕會系結至 AddCharCommand
,其 CommandParameter
與 出現在 Button上的字元相同: