Model-View-ViewModel(MVVM)体系结构模式是在XAML的基础上发明的。 该模式强制三个软件层之间的分离 - XAML用户界面,称为视图; 基础数据,称为模型; 以及View和Model之间的中介,称为ViewModel。 View和ViewModel通常通过XAML文件中定义的数据绑定进行连接。 视图的BindingContext通常是ViewModel的一个实例。
一个简单的ViewModel
作为ViewModels的介绍,我们先来看一个没有的程序。 早些时候,您看到了如何定义一个新的XML名称空间声明,以允许XAML文件引用其他程序集中的类。 这是一个为System命名空间定义XML名称空间声明的程序:
-
xmlns:sys="clr-namespace:System;assembly=mscorlib"
该程序可以使用x:Static从静态DateTime.Now属性获取当前日期和时间,并将该DateTime值设置为StackLayout上的BindingContext:
-
<StackLayout BindingContext="{x:Static sys:DateTime.Now}" …>
BindingContext是一个非常特殊的属性:当你在一个元素上设置BindingContext时,它被该元素的所有子元素继承。 这意味着StackLayout的所有子节点都具有相同的BindingContext,并且可以包含对该对象属性的简单绑定。
在One-Shot DateTime程序中,其中两个子项包含对该DateTime值的属性的绑定,但另外两个子项包含似乎缺少绑定路径的绑定。 这意味着DateTime值本身用于StringFormat:
-
<ContentPage xmlns=""
-
xmlns:x=""
-
xmlns:sys="clr-namespace:System;assembly=mscorlib"
-
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>
当然,最大的问题是,页面初建时的日期和时间是一次设置的,绝不会改变:
-
<ContentPage xmlns=""
-
xmlns:x=""
-
xmlns:sys="clr-namespace:System;assembly=mscorlib"
-
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的角度来看,Model和ViewModel是完全用代码编写的类。 View通常是一个XAML文件,通过数据绑定引用ViewModel中定义的属性。
一个合适的Model对于ViewModel是无知的,一个合适的ViewModel对这个View是无知的。但是,程序员通常会将ViewModel公开的数据类型定制为与特定用户界面相关的数据类型。例如,如果一个Model访问包含8位字符ASCII字符串的数据库,则ViewModel需要将这些字符串转换为Unicode字符串,以便在用户界面中独占使用Unicode。
在MVVM的简单例子中(例如这里所示的例子),通常根本不存在Model,而模式只涉及与数据绑定关联的View和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;
-
}
-
}
-
}
-
}
ViewModels通常实现INotifyPropertyChanged接口,这意味着只要其中一个属性发生变化,该类就会触发一个PropertyChanged事件。 Xamarin.Forms中的数据绑定机制将一个处理程序附加到此PropertyChanged事件,以便在属性更改时通知它,并使目标更新为新值。
基于这个ViewModel的时钟可以像这样简单:
-
<ContentPage xmlns=""
-
xmlns:x=""
-
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如何使用属性元素标签设置为Label的BindingContext。 或者,您可以在Resources集合中实例化ClockViewModel,并通过StaticResource标记扩展将其设置为BindingContext。 或者,代码隐藏文件可以实例化ViewModel。
标签的文本属性上的绑定标记扩展名格式的日期时间属性。 这是显示器:
通过使用句点分隔属性,也可以访问ViewModel的DateTime属性的单独属性:
-
<Label Text="{Binding DateTime.Second, StringFormat='{0}'}" … >
交互式MVVMWS
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));
-
}
-
}
-
}
对“色相”,“饱和度”和“亮度”属性所做的更改会导致Color属性发生更改,而更改为Color将导致其他三个属性发生更改。 这可能看起来像一个无限循环,除非该类不调用PropertyChanged事件,除非该属性实际上已经改变。 这终止了不可控制的反馈回路。
以下XAML文件包含其Color属性绑定到ViewModel的Color属性的BoxView,以及绑定到Hue,Saturation和Luminosity属性的三个Slider和三个Label视图:
-
<ContentPage xmlns=""
-
xmlns:x=""
-
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从ViewModel初始化。 注意,当ViewModel被实例化时,Color属性被设置为蓝色。 但是滑块的改变也需要为ViewModel中的属性设置一个新的值,然后计算一个新的颜色。
用ViewModels命令
在许多情况下,MVVM模式仅限于处理ViewModel中View数据对象中的数据项:用户界面对象。
但是,View有时需要包含在ViewModel中触发各种操作的按钮。 但是ViewModel不能包含按钮的单击处理程序,因为这将把ViewModel绑定到特定的用户界面范例。
为了允许ViewModel更独立于特定的用户界面对象,但仍允许在ViewModel中调用方法,则存在命令界面。 Xamarin.Forms中的以下元素支持此命令接口:
-
Button
-
MenuItem
-
ToolbarItem
-
SearchBar
-
TextCell (ImageCell也是如此)
-
ListView
-
TapGestureRecognizer
除SearchBar和ListView元素外,这些元素定义了两个属性:
-
Command ,类型是System.Windows.Input.ICommand
-
CommandParameter,类型是Object
SearchBar定义了SearchCommand和SearchCommandParameter属性,而ListView定义了一个ICommand类型的RefreshCommand属性。
ICommand接口定义了两个方法和一个事件:
-
void Execute(object arg)
-
bool CanExecute(object arg)
-
event EventHandler CanExecuteChanged
ViewModel可以定义ICommand类型的属性。然后,您可以将这些属性绑定到每个Button或其他元素的Command属性,或者实现此接口的自定义视图。您可以选择设置CommandParameter属性来标识绑定到此ViewModel属性的各个Button对象(或其他元素)。在内部,只要用户点击Button,传递给Execute方法的CommandParameter,Button就会调用Execute方法。
CanExecute方法和CanExecuteChanged事件用于Button按钮可能当前无效的情况,在这种情况下,Button应该禁用它自己。当Command属性第一次被设置和CanExecuteChanged事件被触发时,Button调用CanExecute。如果CanExecute返回false,则Button将自行禁用,并不会生成执行调用。
这两个类定义了几个构造函数以及ViewModel可以调用的ChangeCanExecute方法来强制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属性的电话号码。
另外还有一个名为DeleteCharCommand的ICommand类型的第二个属性。 这是绑定到一个后退间隔按钮,但该按钮应该被禁用,如果没有字符删除。
下面的键盘不像视觉上那么复杂。 相反,标记已经降到最低,以更清楚地展示命令接口的使用:
出现在该标记中的第一个Button的Command属性绑定到DeleteCharCommand; 剩下的都绑定到AddCharCommand,CommandParameter与Button面上出现的字符相同。 以下是正在实施的计划:
调用异步方法
命令也可以调用异步方法。 这是通过在指定Execute方法时使用async和await关键字来实现的:
-
DownloadCommand = new Command (async () => await DownloadAsync ());
这表明DownloadAsync方法是一个任务,应该等待:
-
async Task DownloadAsync ()
-
{
-
await Task.Run (() => Download ());
-
}
-
-
void Download ()
-
{
-
...
-
}
实现一个导航菜单
包含本系列文章中所有源代码的XamlSamples程序使用ViewModel作为其主页。 这个ViewModel是一个短类的定义,它有三个名为Type,Title和Description的属性,它们包含了每个样例页面的类型,一个标题和一个简短描述。 另外,ViewModel定义了一个名为All的静态属性,它是程序中所有页面的集合:
-
public class PageDataViewModel
-
{
-
public PageDataViewModel(Type type, string title, string description)
-
{
-
Type = type;
-
Title = title;
-
Description = description;
-
}
-
-
public Type Type { private set; get; }
-
-
public string Title { private set; get; }
-
-
public string Description { private set; get; }
-
-
static PageDataViewModel()
-
{
-
All = new List<PageDataViewModel>
-
{
-
// Part 1. Getting Started with XAML
-
new PageDataViewModel(typeof(HelloXamlPage), "Hello, XAML",
-
"Display a Label with many properties set"),
-
-
new PageDataViewModel(typeof(XamlPlusCodePage), "XAML + Code",
-
"Interact with a Slider and Button"),
-
-
// Part 2. Essential XAML Syntax
-
new PageDataViewModel(typeof(GridDemoPage), "Grid Demo",
-
"Explore XAML syntax with the Grid"),
-
-
new PageDataViewModel(typeof(AbsoluteDemoPage), "Absolute Demo",
-
"Explore XAML syntax with AbsoluteLayout"),
-
-
// Part 3. XAML Markup Extensions
-
new PageDataViewModel(typeof(SharedResourcesPage), "Shared Resources",
-
"Using resource dictionaries to share resources"),
-
-
new PageDataViewModel(typeof(StaticConstantsPage), "Static Constants",
-
"Using the x:Static markup extensions"),
-
-
new PageDataViewModel(typeof(RelativeLayoutPage), "Relative Layout",
-
"Explore XAML markup extensions"),
-
-
// Part 4. Data Binding Basics
-
new PageDataViewModel(typeof(SliderBindingsPage), "Slider Bindings",
-
"Bind properties of two views on the page"),
-
-
new PageDataViewModel(typeof(SliderTransformsPage), "Slider Transforms",
-
"Use Sliders with reverse bindings"),
-
-
new PageDataViewModel(typeof(ListViewDemoPage), "ListView Demo",
-
"Use a ListView with data bindings"),
-
-
// Part 5. From Data Bindings to MVVM
-
new PageDataViewModel(typeof(OneShotDateTimePage), "One-Shot DateTime",
-
"Obtain the current DateTime and display it"),
-
-
new PageDataViewModel(typeof(ClockPage), "Clock",
-
"Dynamically display the current time"),
-
-
new PageDataViewModel(typeof(HslColorScrollPage), "HSL Color Scroll",
-
"Use a view model to select HSL colors"),
-
-
new PageDataViewModel(typeof(KeypadPage), "Keypad",
-
"Use a view model for numeric keypad logic")
-
};
-
}
-
-
public static IList<PageDataViewModel> All { private set; get; }
-
}
MainPage的XAML文件定义了一个ListBox,其ItemsSource属性被设置为All属性,并包含一个TextCell用于显示每个页面的Title和Description属性:
-
<ContentPage xmlns=""
-
xmlns:x=""
-
xmlns:local="clr-namespace:XamlSamples"
-
x:Class="XamlSamples.MainPage"
-
Padding="5, 0"
-
Title="XAML Samples">
-
-
<ListView ItemsSource="{x:Static local:PageDataViewModel.All}"
-
ItemSelected="OnListViewItemSelected">
-
<ListView.ItemTemplate>
-
<DataTemplate>
-
<TextCell Text="{Binding Title}"
-
Detail="{Binding Description}" />
-
</DataTemplate>
-
</ListView.ItemTemplate>
-
</ListView>
-
</ContentPage>
页面显示在一个可滚动列表中:
代码隐藏文件中的处理程序在用户选择某个项目时被触发。 该处理程序将ListBox的SelectedItem属性设置为null,然后实例化所选页面并导航到它:
-
private async void OnListViewItemSelected(object sender, SelectedItemChangedEventArgs args)
-
{
-
(sender as ListView).SelectedItem = null;
-
-
if (args.SelectedItem != null)
-
{
-
PageDataViewModel pageData = args.SelectedItem as PageDataViewModel;
-
Page page = (Page)Activator.CreateInstance(pageData.Type);
-
await Navigation.PushAsync(page);
-
}
-
}
概要
XAML是在Xamarin.Forms应用程序中定义用户界面的强大工具,特别是在使用数据绑定和MVVM时。 其结果是一个干净,优雅,并可能toolable表示的用户界面代码中的所有后台支持。