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="http://xamarin.com/schemas/2014/forms"
- xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
- 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="http://xamarin.com/schemas/2014/forms"
- xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
- 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="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>
标签的文本属性上的绑定标记扩展名格式的日期时间属性。 这是显示器:
通过使用句点分隔属性,也可以访问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="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>
用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 Commandstring>((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="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="#"
- 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 ListPageDataViewModel>
- {
- // 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 IListPageDataViewModel> All { private set; get; }
- }
点击(此处)折叠或打开
- ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
- xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
- 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表示的用户界面代码中的所有后台支持。