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"
点击(此处)折叠或打开
- <StackLayout BindingContext="{x:Static sys:DateTime.Now}" …>
在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;
-
}
-
}
-
}
- }
基于这个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>
标签的文本属性上的绑定标记扩展名格式的日期时间属性。 这是显示器:
通过使用句点分隔属性,也可以访问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));
-
}
-
}
- }
以下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>
用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
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));
-
}
-
}
- }
另外还有一个名为DeleteCharCommand的ICommand类型的第二个属性。 这是绑定到一个后退间隔按钮,但该按钮应该被禁用,如果没有字符删除。
下面的键盘不像视觉上那么复杂。 相反,标记已经降到最低,以更清楚地展示命令接口的使用:
点击(此处)折叠或打开
-
<ContentPage xmlns=""
-
xmlns:x=""
-
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="#"
-
Command="{Binding AddCharCommand}"
-
CommandParameter="#"
-
Grid.Row="4" Grid.Column="2" />
-
</Grid>
- </ContentPage>
调用异步方法
命令也可以调用异步方法。 这是通过在指定Execute方法时使用async和await关键字来实现的:
点击(此处)折叠或打开
- DownloadCommand = new Command (async () => await 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; }
- }
点击(此处)折叠或打开
-
<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表示的用户界面代码中的所有后台支持。