天天看點

第5部分.把資料綁定到MVVM——Model-View-ViewModel體系結構的介紹 一個簡單的ViewModel 互動式MVVMWS 用ViewModels指令 實作一個導航菜單 概要

Model-View-ViewModel(MVVM)體系結構模式是在XAML的基礎上發明的。 該模式強制三個軟體層之間的分離 - XAML使用者界面,稱為視圖; 基礎資料,稱為模型; 以及View和Model之間的中介,稱為ViewModel。 View和ViewModel通常通過XAML檔案中定義的資料綁定進行連接配接。 視圖的BindingContext通常是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="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>

當然,最大的問題是,頁面初建時的日期和時間是一次設定的,絕不會改變:

第5部分.把資料綁定到MVVM——Model-View-ViewModel體系結構的介紹 一個簡單的ViewModel 互動式MVVMWS 用ViewModels指令 實作一個導航菜單 概要

一個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的時鐘可以像這樣簡單:

             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>

請注意ClockViewModel如何使用屬性元素标簽設定為Label的BindingContext。 或者,您可以在Resources集合中執行個體化ClockViewModel,并通過StaticResource标記擴充将其設定為BindingContext。 或者,代碼隐藏檔案可以執行個體化ViewModel。

标簽的文本屬性上的綁定标記擴充名格式的日期時間屬性。 這是顯示器:

第5部分.把資料綁定到MVVM——Model-View-ViewModel體系結構的介紹 一個簡單的ViewModel 互動式MVVMWS 用ViewModels指令 實作一個導航菜單 概要

通過使用句點分隔屬性,也可以通路ViewModel的DateTime屬性的單獨屬性:

Label Text="{Binding DateTime.Second, StringFormat='{0}'}" … >

MVVM通常用于基于底層資料模型的互動式視圖的雙向資料綁定。

這是一個名為HslViewModel的類,它将Color值轉換為Hue,Saturation和Luminosity值,反之亦然:

    public class HslViewModel : INotifyPropertyChanged

        double hue, saturation, luminosity;

        Color color;

        public double Hue

                if (hue != value)

                    hue = value;

                    OnPropertyChanged("Hue");

                    SetNewColor();

                return hue;

        public double Saturation

                if (saturation != value)

                    saturation = value;

                    OnPropertyChanged("Saturation");

                return saturation;

        public double Luminosity

                if (luminosity != value)

                    luminosity = value;

                    OnPropertyChanged("Luminosity");

                return luminosity;

        public Color Color

                if (color != value)

                    color = value;

                    OnPropertyChanged("Color");

                    Hue = value.Hue;

                    Saturation = value.Saturation;

                    Luminosity = value.Luminosity;

                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視圖:

             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}'}"

        Slider Value="{Binding Saturation, Mode=TwoWay}" />

        Label Text="{Binding Luminosity, StringFormat='Luminosity = {0:F2}'}"

        Slider Value="{Binding Luminosity, Mode=TwoWay}" />

每個Label上的綁定是預設的OneWay。 它隻需要顯示值。 但每個滑塊的綁定是雙向的。 這允許Slider從ViewModel初始化。 注意,當ViewModel被執行個體化時,Color屬性被設定為藍色。 但是滑塊的改變也需要為ViewModel中的屬性設定一個新的值,然後計算一個新的顔色。

第5部分.把資料綁定到MVVM——Model-View-ViewModel體系結構的介紹 一個簡單的ViewModel 互動式MVVMWS 用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.Windows.Input;

    class KeypadViewModel : INotifyPropertyChanged

        string inputString = "";

        string displayText = "";

        char[] specialChars = { '*', '#' };

        // 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

                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(3, 3),

                                          str.Substring(6));

            return formatted;

        protected void OnPropertyChanged(string propertyName)

這個ViewModel假定AddCharCommand屬性綁定到幾個按鈕的Command屬性(或其他任何具有指令接口的屬性),每個按鈕都由CommandParameter辨別。 這些按鈕将字元添加到InputString屬性,然後将其格式化為DisplayText屬性的電話号碼。

另外還有一個名為DeleteCharCommand的ICommand類型的第二個屬性。 這是綁定到一個後退間隔按鈕,但該按鈕應該被禁用,如果沒有字元删除。

下面的鍵盤不像視覺上那麼複雜。 相反,标記已經降到最低,以更清楚地展示指令接口的使用:

             x:Class="XamlSamples.KeypadPage"

             Title="Keypad Page">

    Grid HorizontalOptions="Center"

          VerticalOptions="Center">

        Grid.BindingContext>

            local:KeypadViewModel />

        /Grid.BindingContext>

        Grid.RowDefinitions>

            RowDefinition Height="Auto" />

        /Grid.RowDefinitions>

        Grid.ColumnDefinitions>

            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"

                CommandParameter="2"

                Grid.Row="1" Grid.Column="1" />

        Button Text="3"

                CommandParameter="3"

                Grid.Row="1" Grid.Column="2" />

        Button Text="4"

                CommandParameter="4"

                Grid.Row="2" Grid.Column="0" />

        Button Text="5"

                CommandParameter="5"

                Grid.Row="2" Grid.Column="1" />

        Button Text="6"

                CommandParameter="6"

                Grid.Row="2" Grid.Column="2" />

        Button Text="7"

                CommandParameter="7"

                Grid.Row="3" Grid.Column="0" />

        Button Text="8"

                CommandParameter="8"

                Grid.Row="3" Grid.Column="1" />

        Button Text="9"

                CommandParameter="9"

                Grid.Row="3" Grid.Column="2" />

        Button Text="*"

                CommandParameter="*"

                Grid.Row="4" Grid.Column="0" />

        Button Text="0"

                CommandParameter="0"

                Grid.Row="4" Grid.Column="1" />

        Button Text="#"

                CommandParameter="#"

                Grid.Row="4" Grid.Column="2" />

    /Grid>

出現在該标記中的第一個Button的Command屬性綁定到DeleteCharCommand; 剩下的都綁定到AddCharCommand,CommandParameter與Button面上出現的字元相同。 以下是正在實施的計劃:

第5部分.把資料綁定到MVVM——Model-View-ViewModel體系結構的介紹 一個簡單的ViewModel 互動式MVVMWS 用ViewModels指令 實作一個導航菜單 概要

指令也可以調用異步方法。 這是通過在指定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 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; }

MainPage的XAML檔案定義了一個ListBox,其ItemsSource屬性被設定為All屬性,并包含一個TextCell用于顯示每個頁面的Title和Description屬性:

             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>

頁面顯示在一個可滾動清單中:

第5部分.把資料綁定到MVVM——Model-View-ViewModel體系結構的介紹 一個簡單的ViewModel 互動式MVVMWS 用ViewModels指令 實作一個導航菜單 概要

代碼隐藏檔案中的處理程式在使用者選擇某個項目時被觸發。 該處理程式将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表示的使用者界面代碼中的所有背景支援。