Xamarin 从数据绑定到 MVVM
模型-视图-ViewModel (MVVM)体系结构模式是在概念上利用 XAML 来构建的。此模式强制分离三个软件层(XAML 用户界面,称作视图):基本数据,称为模型;在视图和模型之间使用中间名为 ViewModel。视图和 ViewModel 通常通过 XAML 文件中定义的数据绑定连接。视图的 BindingContext 通常是 ViewModel 的实例。
简单的 ViewModel
作为 Viewmodel 的简介,让我们先看一看没有一个程序的程序。 之前,你已了解如何定义新的 XML 命名空间声明,以允许 XAML 文件引用其他程序集中的类。 下面是一个程序,它定义命名空间的 XML 命名空间声明 System :
xmlns:sys="clr-namespace:System;assembly=netstandard"
程序可以使用 x:Static 从静态属性获取当前日期和时间 DateTime.Now ,并将 DateTime 该值设置为 BindingContext 上的 StackLayout :
<StackLayout BindingContext="{x:Static sys:DateTime.Now}" …>
BindingContext是一个特殊属性:当在元素上设置时,该元素的所有子级都将继承该属性 BindingContext 。 这意味着的所有子级 StackLayout 都具有相同的 BindingContext ,并且它们可以包含与该对象的属性的简单绑定。
在一键式 DateTime程序中,有两个子项包含对该值的属性的绑定 DateTime ,但两个其他子级包含似乎缺少绑定路径的绑定。 这意味着 DateTime 值本身用于 StringFormat :
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
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">
<StackLayout BindingContext="{x:Static sys:DateTime.Now}"
HorizontalOptions="Center"
VerticalOptions="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}'}" />
</StackLayout>
</ContentPage>
问题在于,在第一次生成页面时,日期和时间设置一次,并且永远不会发生更改:
XAML 文件可以显示始终显示当前时间的时钟,但它需要一些代码来帮助你解决问题。当考虑 MVVM 时,模型和 ViewModel 是完全用代码编写的类。 视图通常是一个 XAML 文件,它引用 ViewModel 中通过数据绑定定义的属性。
适当的模型是未知的 ViewModel,并有适当的 ViewModel 未知的。 但是,通常情况下,程序员会将 ViewModel 公开的数据类型定制到与特定用户界面相关联的数据类型。 例如,如果某个模型访问的数据库包含8位字符 ASCII 字符串,则 ViewModel 需要在这两个字符串之间进行转换,以便在用户界面中独占使用 Unicode。
在 MVVM 的简单示例(如此处所示的示例)中,通常根本没有模型,该模式只涉及与数据绑定关联的视图和 ViewModel。
下面是一个 ViewModel,其中只包含一个名为的属性 DateTime ,该属性 DateTime 每秒更新一次该属性:
using System;
using System.ComponentModel;
using Xamarin.Forms;
namespace XamlSamples
{
class ClockViewModel : INotifyPropertyChanged
{
DateTime dateTime;
public event PropertyChangedEventHandler PropertyChanged;
public ClockViewModel()
{
this.DateTime = DateTime.Now;
Device.StartTimer(TimeSpan.FromSeconds(1), () =>
{
this.DateTime = DateTime.Now;
return true;
});
}
public DateTime DateTime
{
set
{
if (dateTime != value)
{
dateTime = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("DateTime"));
}
}
}
get
{
return dateTime;
}
}
}
}
Viewmodel 通常实现 INotifyPropertyChanged 接口,这意味着每当类的属性发生更改时,类就会触发 PropertyChanged 事件。 中的数据绑定机制将 Xamarin.Forms 处理程序附加到此 PropertyChanged 事件,以便在属性发生更改时通知该事件,并使用新值更新目标。
基于此 ViewModel 的时钟非常简单,如下所示:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
x:Class="XamlSamples.ClockPage"
Title="Clock Page">
<Label Text="{Binding DateTime, StringFormat='{0:T}'}"
FontSize="Large"
HorizontalOptions="Center"
VerticalOptions="Center">
<Label.BindingContext>
<local:ClockViewModel />
</Label.BindingContext>
</Label>
</ContentPage>
请注意如何 ClockViewModel BindingContext Label 使用属性元素标记将设置为的。 或者,可以 ClockViewModel 在集合中实例化, Resources 并 BindingContext 通过标记扩展将其设置为 StaticResource 。 或者,代码隐藏文件可以实例化 ViewModel。
Binding的属性的标记扩展 Text Label 设置属性的格式 DateTime 。 显示内容如下:
还可以 DateTime 通过用句点分隔属性,来访问 ViewModel 属性的各个属性:
<Label Text="{Binding DateTime.Second, StringFormat='{0}'}" … >
交互式 MVVM
对于基于基础数据模型的交互式视图,MVVM 通常与双向数据绑定一起使用。
下面是一个名为 HslViewModel 的类,它将 Color 值转换为 Hue 、 Saturation 和 Luminosity 值,反之亦然:
using System;
using System.ComponentModel;
using Xamarin.Forms;
namespace XamlSamples
{
public class HslViewModel : INotifyPropertyChanged
{
double hue, saturation, luminosity;
Color color;
public event PropertyChangedEventHandler PropertyChanged;
public double Hue
{
set
{
if (hue != value)
{
hue = value;
OnPropertyChanged("Hue");
SetNewColor();
}
}
get
{
return hue;
}
}
public double Saturation
{
set
{
if (saturation != value)
{
saturation = value;
OnPropertyChanged("Saturation");
SetNewColor();
}
}
get
{
return saturation;
}
}
public double Luminosity
{
set
{
if (luminosity != value)
{
luminosity = value;
OnPropertyChanged("Luminosity");
SetNewColor();
}
}
get
{
return luminosity;
}
}
public Color Color
{
set
{
if (color != value)
{
color = value;
OnPropertyChanged("Color");
Hue = value.Hue;
Saturation = value.Saturation;
Luminosity = value.Luminosity;
}
}
get
{
return color;
}
}
void SetNewColor()
{
Color = Color.FromHsla(Hue, Saturation, Luminosity);
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
对 Hue 、和属性的更改将 Saturation Luminosity 导致 Color 属性发生更改,并将更改为 Color 导致其他三个属性更改。 这似乎是一个无限循环,只不过类不调用 PropertyChanged 事件,除非属性已更改。 这会将一个端置于另一个不可控的反馈循环。
下面的 XAML 文件包含, BoxView 其 Color 属性绑定到 ViewModel 的 Color 属性,以及三个 Slider 和三个 Label 绑定到 Hue 、 Saturation 和属性的视图 Luminosity :
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
x:Class="XamlSamples.HslColorScrollPage"
Title="HSL Color Scroll Page">
<ContentPage.BindingContext>
<local:HslViewModel Color="Aqua" />
</ContentPage.BindingContext>
<StackLayout Padding="10, 0">
<BoxView Color="{Binding Color}"
VerticalOptions="FillAndExpand" />
<Label Text="{Binding Hue, StringFormat='Hue = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Hue, Mode=TwoWay}" />
<Label Text="{Binding Saturation, StringFormat='Saturation = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Saturation, Mode=TwoWay}" />
<Label Text="{Binding Luminosity, StringFormat='Luminosity = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Luminosity, Mode=TwoWay}" />
</StackLayout>
</ContentPage>
每个上的绑定 Label 都是默认值 OneWay 。 它只需要显示值。 但每个上的绑定 Slider 是 TwoWay 。 这允许 Slider 从 ViewModel 进行初始化。 请注意,在 Color Aqua 对 ViewModel 进行实例化时,属性设置为。 但中的更改 Slider 还需要为 ViewModel 中的属性设置新值,然后计算新的颜色。
用 Viewmodel 进行命令
在许多情况下,MVVM 模式限制为数据项目的操作: ViewModel 中的视图并行数据对象中的用户界面对象。
但有时,视图需要包含在 ViewModel 中触发各种操作的按钮。 但 ViewModel 不能包含 Clicked 按钮的处理程序,因为这会将 ViewModel 关联到特定的用户界面模式。
若要允许 Viewmodel 更独立于特定用户界面对象,但仍允许在 ViewModel 中调用方法,则可以使用命令界面。 以下元素支持此命令界面 Xamarin.Forms :
ButtonMenuItemToolbarItemSearchBarTextCell(因此还会ImageCell)ListViewTapGestureRecognizer
除了 SearchBar 和 ListView 元素,这些元素定义了两个属性:
Command类型为System.Windows.Input.ICommandCommandParameter类型为Object
SearchBar定义 SearchCommand 和 SearchCommandParameter 属性,而 ListView 定义 RefreshCommand 类型的属性 ICommand 。
ICommand接口定义了两个方法和一个事件:
void Execute(object arg)bool CanExecute(object arg)event EventHandler CanExecuteChanged
ViewModel 可以定义类型的属性 ICommand 。 然后,可以将这些属性绑定到 Command 每个 Button 或其他元素的属性,或者可能绑定到实现此接口的自定义视图。 您可以选择将 CommandParameter 属性设置为标识 Button 绑定到此 ViewModel 属性的各个对象(或其他元素)。 在内部, Button Execute 每当用户点击 Button ,并将其传递给方法时,都会调用方法 Execute CommandParameter 。
CanExecute方法和 CanExecuteChanged 事件用于在 Button 点击可能当前无效的情况下,在这种情况下, Button 应禁用自身。 Button CanExecute Command 第一次设置属性和 CanExecuteChanged 触发事件时调用。 如果 CanExecute 返回 false ,则将 Button 禁用自身,而不会生成 Execute 调用。
有关将命令添加到 Viewmodel 的帮助,请 Xamarin.Forms 定义实现的两个类 ICommand : Command , Command<T> 其中 T 是和的参数的 Execute 类型 CanExecute 。 这两个类定义了多个构造函数,以及一个 ChangeCanExecute 方法,ViewModel 可以调用该方法来强制 Command 对象触发 CanExecuteChanged 事件。
下面是用于输入电话号码的简单键盘的 ViewModel。 请注意, Execute 和 CanExecute 方法在构造函数中定义为 lambda 函数:
using System;
using System.ComponentModel;
using System.Windows.Input;
using Xamarin.Forms;
namespace XamlSamples
{
class KeypadViewModel : INotifyPropertyChanged
{
string inputString = "";
string displayText = "";
char[] specialChars = { '*', '#' };
public event PropertyChangedEventHandler PropertyChanged;
// Constructor
public KeypadViewModel()
{
AddCharCommand = new Command<string>((key) =>
{
// Add the key to the input string.
InputString += key;
});
DeleteCharCommand = new Command(() =>
{
// Strip a character from the input string.
InputString = InputString.Substring(0, InputString.Length - 1);
},
() =>
{
// Return true if there's something to delete.
return InputString.Length > 0;
});
}
// Public properties
public string InputString
{
protected set
{
if (inputString != value)
{
inputString = value;
OnPropertyChanged("InputString");
DisplayText = FormatText(inputString);
// Perhaps the delete button must be enabled/disabled.
((Command)DeleteCharCommand).ChangeCanExecute();
}
}
get { return inputString; }
}
public string DisplayText
{
protected set
{
if (displayText != value)
{
displayText = value;
OnPropertyChanged("DisplayText");
}
}
get { return displayText; }
}
// ICommand implementations
public ICommand AddCharCommand { protected set; get; }
public ICommand DeleteCharCommand { protected set; get; }
string FormatText(string str)
{
bool hasNonNumbers = str.IndexOfAny(specialChars) != -1;
string formatted = str;
if (hasNonNumbers || str.Length < 4 || str.Length > 10)
{
}
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;
}
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
此 ViewModel 假设将 AddCharCommand 属性绑定到 Command 多个按钮的属性(或具有命令界面的任何其他按钮),其中每个按钮都由标识 CommandParameter 。 这些按钮将字符添加到 InputString 属性,然后将其格式化为属性的电话号码 DisplayText 。
还有另一个名为的类型的 ICommand 属性 DeleteCharCommand 。 此项已绑定到后退间距按钮,但如果没有要删除的字符,则应禁用该按钮。
以下小键盘并不像它那样非常复杂。 相反,此标记已缩小为最小值,以演示更清晰地使用命令界面:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
x:Class="XamlSamples.KeypadPage"
Title="Keypad Page">
<Grid HorizontalOptions="Center"
VerticalOptions="Center">
<Grid.BindingContext>
<local:KeypadViewModel />
</Grid.BindingContext>
<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>
<!-- Internal Grid for top row of items -->
<Grid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Frame Grid.Column="0"
OutlineColor="Accent">
<Label Text="{Binding DisplayText}" />
</Frame>
<Button Text="⇦"
Command="{Binding DeleteCharCommand}"
Grid.Column="1"
BorderWidth="0" />
</Grid>
<Button Text="1"
Command="{Binding AddCharCommand}"
CommandParameter="1"
Grid.Row="1" Grid.Column="0" />
<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" Grid.Column="0" />
<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" Grid.Column="0" />
<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" Grid.Column="0" />
<Button Text="0"
Command="{Binding AddCharCommand}"
CommandParameter="0"
Grid.Row="4" Grid.Column="1" />
<Button Text=


