天天看點

第十八章:MVVM(九)

幾乎是一個電腦

現在是時候使用具有Execute和CanExecute方法的ICommand對象制作更複雜的ViewModel。 下一個程式幾乎就像一個電腦,隻是它隻添加了一系列數字。 ViewModel名為AdderViewModel,該程式名為AddingMachine。

我們先來看一下截圖:

第十八章:MVVM(九)

在頁面頂部,您可以看到已輸入和添加的一系列數字的曆史記錄。這是ScrollView中的Label,是以它可以變得相當長。

這些數字的總和顯示在鍵盤上方的Entry視圖中。通常,該條目視圖包含您鍵入的數字,但是在您點選鍵盤右側的大加号後,條目将顯示累計金額,加号按鈕将被禁用。您需要開始輸入nother數字,以便累積的總和消失,并且需要啟用帶加号的按鈕。同樣,隻要您開始輸入,就會啟用倒退按鈕。

這些不是唯一可以禁用的鍵。當您鍵入的數字已經有小數點時,小數點被禁用,當數字包含16個字元時,所有數字鍵都被禁用。這是為了避免條目中的數字變得太長而無法顯示。

禁用這些按鈕是在ICommand接口中實作CanExecute方法的結果。

AdderViewModel類位于Xamarin.FormsBook.Toolkit庫中,并派生自ViewModelBase。以下是具有所有公共屬性及其支援字段的類的一部分:

public class AdderViewModel : ViewModelBase
{
    string currentEntry = "0";
    string historyString = "";
    __
    public string CurrentEntry 
    { 
        private set { SetProperty(ref currentEntry, value); }
        get { return currentEntry; }
    }
    public string HistoryString 
    { 
        private set { SetProperty(ref historyString, value); }
        get { return historyString; }
    }
    public ICommand ClearCommand { private set; get; }
    public ICommand ClearEntryCommand { private set; get; }
    public ICommand BackspaceCommand { private set; get; }
    public ICommand NumericCommand { private set; get; }
    public ICommand DecimalPointCommand { private set; get; }
    public ICommand AddCommand { private set; get; }
    __
}           

所有屬性都有私有set通路器。 類型字元串的兩個屬性僅在内部基于鍵抽頭設定,并且類型ICommand的屬性在AdderViewModel構造函數中設定(稍後您将看到)。

這八個公共屬性是AddderViewModel中AddingMachine項目中XAML檔案需要了解的唯一部分。 這是XAML檔案。 它包含一個用于在縱向和橫向模式之間切換的兩行和兩列主網格,以及一個Label,Entry和15 Button元素,所有這些元素都綁定到AdderViewModel的八個公共屬性之一。 請注意,所有10位數按鈕的Command屬性都綁定到NumericCommand屬性,并且按鈕由CommandParameter屬性區分。 此CommandParameter屬性的設定作為參數傳遞給Execute和CanExecute方法:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="AddingMachine.AddingMachinePage"
             SizeChanged="OnPageSizeChanged">
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness"
                    iOS="10, 20, 10, 10"
                    Android="10"
                    WinPhone="10" />
    </ContentPage.Padding>
    <Grid x:Name="mainGrid">
        <!-- Initialized for Portrait mode. -->
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="0" />
        </Grid.ColumnDefinitions>
 
        <!-- History display. -->
        <ScrollView Grid.Row="0" Grid.Column="0"
                    Padding="5, 0">
            <Label Text="{Binding HistoryString}" />
        </ScrollView>
 
        <!-- Keypad. -->
        <Grid x:Name="keypadGrid"
              Grid.Row="1" Grid.Column="0"
              RowSpacing="2"
              ColumnSpacing="2"
              WidthRequest="240"
              HeightRequest="360"
              VerticalOptions="Center"
              HorizontalOptions="Center">
            <Grid.Resources>
                <ResourceDictionary>
                    <Style TargetType="Button">
                        <Setter Property="FontSize" Value="Large" />
                        <Setter Property="BorderWidth" Value="1" />
                    </Style>
                </ResourceDictionary>
            </Grid.Resources>
            <Label Text="{Binding CurrentEntry}"
                   Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="4"
                   FontSize="Large"
                   LineBreakMode="HeadTruncation"
                   VerticalOptions="Center"
                   HorizontalTextAlignment="End" />
            <Button Text="C"
                    Grid.Row="1" Grid.Column="0"
                    Command="{Binding ClearCommand}" />
            <Button Text="CE"
                    Grid.Row="1" Grid.Column="1"
                    Command="{Binding ClearEntryCommand}" />
            <Button Text="&#x21E6;"
                    Grid.Row="1" Grid.Column="2"
                    Command="{Binding BackspaceCommand}" />

            <Button Text="+"
                    Grid.Row="1" Grid.Column="3" Grid.RowSpan="5"
                    Command="{Binding AddCommand}" />
            <Button Text="7"
                    Grid.Row="2" Grid.Column="0"
                    Command="{Binding NumericCommand}"
                    CommandParameter="7" />
            <Button Text="8"
                    Grid.Row="2" Grid.Column="1"
                    Command="{Binding NumericCommand}"
                    CommandParameter="8" />
            <Button Text="9"
                    Grid.Row="2" Grid.Column="2"
                    Command="{Binding NumericCommand}"
                    CommandParameter="9" />
            <Button Text="4"
                    Grid.Row="3" Grid.Column="0"
                    Command="{Binding NumericCommand}"
                    CommandParameter="4" />
 
           <Button Text="5"
                    Grid.Row="3" Grid.Column="1"
                    Command="{Binding NumericCommand}"
                    CommandParameter="5" />
 
            <Button Text="6"
                    Grid.Row="3" Grid.Column="2"
                    Command="{Binding NumericCommand}"
                    CommandParameter="6" />
            <Button Text="1"
                    Grid.Row="4" Grid.Column="0"
                    Command="{Binding NumericCommand}"
                    CommandParameter="1" />
 
            <Button Text="2"
                    Grid.Row="4" Grid.Column="1"
                    Command="{Binding NumericCommand}"
                    CommandParameter="2" />
 
            <Button Text="3"
                    Grid.Row="4" Grid.Column="2"
                    Command="{Binding NumericCommand}"
                    CommandParameter="3" />
            <Button Text="0"
                    Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="2"
                    Command="{Binding NumericCommand}"
                    CommandParameter="0" />

            <Button Text="&#x00B7;"
                    Grid.Row="5" Grid.Column="2"
                    Command="{Binding DecimalPointCommand}" />
        </Grid>
    </Grid>
</ContentPage>           

您在XAML檔案中找不到的是對AdderViewModel的引用。由于您很快就會看到的原因,AdderViewModel在代碼中執行個體化。

添加機器邏輯的核心在六個ICommand屬性的Execute和CanExecute方法中。這些屬性都在下面顯示的AdderViewModel構造函數中初始化,而Execute和CanExecute方法都是lambda函數。

當Command構造函數中隻出現一個lambda函數時,這就是Execute方法(如參數名稱所示),并且始終啟用Button。這是ClearCommand和ClearEntryCommand的情況。

所有其他Command構造函數都有兩個lambda函數。第一個是Execute方法,第二個是CanExecute方法。如果要啟用Buttons,則CanExecute方法傳回true,否則傳回false。

除了NumericCommand之外,所有ICommand屬性都使用Command類的非泛型形式設定,NumericCommand需要一個Execute和CanExecute方法的參數來識别已敲擊的鍵:

public class AdderViewModel : ViewModelBase
{
    __
    bool isSumDisplayed = false;
    double accumulatedSum = 0;
    public AdderViewModel()
    {
        ClearCommand = new Command(
            execute: () =>
            {
                HistoryString = "";
                accumulatedSum = 0;
                CurrentEntry = "0";
                isSumDisplayed = false;
                RefreshCanExecutes();
            });
        ClearEntryCommand = new Command(
            execute: () =>
            {
                CurrentEntry = "0";
                isSumDisplayed = false;
                RefreshCanExecutes();
            });
        BackspaceCommand = new Command(
            execute: () =>
            {
                CurrentEntry = CurrentEntry.Substring(0, CurrentEntry.Length - 1);
                if (CurrentEntry.Length == 0)
                {
                    CurrentEntry = "0";
                }
                RefreshCanExecutes();
            },
            canExecute: () =>
            {
                return !isSumDisplayed && (CurrentEntry.Length > 1 ||            CurrentEntry[0] != '0');
            });
        NumericCommand = new Command<string>(
            execute: (string parameter) =>
            {
                if (isSumDisplayed || CurrentEntry == "0")
                    CurrentEntry = parameter;
                else
                    CurrentEntry += parameter;
                isSumDisplayed = false;
                RefreshCanExecutes();
            },
            canExecute: (string parameter) =>
            {
                return isSumDisplayed || CurrentEntry.Length < 16;
            });
        DecimalPointCommand = new Command(
            execute: () =>
            {
                if (isSumDisplayed)
                    CurrentEntry = "0.";
                else
                    CurrentEntry += ".";
                isSumDisplayed = false;
                RefreshCanExecutes();
            },
            canExecute: () =>
            {
                return isSumDisplayed || !CurrentEntry.Contains(".");
            });
        AddCommand = new Command(
            execute: () =>
            {
                double value = Double.Parse(CurrentEntry);
                HistoryString += value.ToString() + " + ";
                accumulatedSum += value;
                CurrentEntry = accumulatedSum.ToString();
                isSumDisplayed = true;
                RefreshCanExecutes();
            },
            canExecute: () =>
            {
                return !isSumDisplayed;
            });
    }
    void RefreshCanExecutes()
    {
        ((Command)BackspaceCommand).ChangeCanExecute();
        ((Command)NumericCommand).ChangeCanExecute();
        ((Command)DecimalPointCommand).ChangeCanExecute();
        ((Command)AddCommand).ChangeCanExecute();
    }
    __
}            

所有的Execute方法都是通過在構造函數之後調用名為RefreshCanExecute的方法來結束的。此方法調用實作CanExecute方法的四個Command對象中的每一個的ChangeCanExecute方法。該方法調用導緻Command對象觸發ChangeCanExecute事件。每個Button通過再次調用CanExecute方法來響應該事件,以确定是否應該啟用Button。

每個Execute方法都不需要調用所有四個ChangeCanExecute方法。例如,當NumericCommand的Execute方法執行時,不需要調用DecimalPointCommand的ChangeCanExecute方法。然而,事實證明,在邏輯和代碼整合方面更容易 - 隻需在每次按鍵後調用它們。

您可能更習慣将這些Execute和CanExecute方法實作為正常方法而不是lambda函數。或者你可能更舒服隻有一個Command對象來處理所有的鍵。每個鍵都可以有一個辨別CommandParameter字元串,您可以使用switch和case語句區分它們。

有很多方法可以實作指令邏輯,但應該清楚的是,指令的使用傾向于以靈活和理想的方式構造代碼。

一旦添加邏輯到位,為什麼不為減法,乘法和除法添加幾個按鈕?

好吧,增強邏輯來接受多個操作而不僅僅是一個操作并不是那麼容易。如果程式支援多個操作,則當使用者鍵入其中一個操作鍵時,需要儲存該操作以等待下一個數字。隻有在下一個數字完成後(通過按下另一個操作鍵或等号鍵發出信号)才會應用儲存的操作。

更簡單的方法是編寫反向波蘭表示法(RPN)電腦,其中操作在第二個數字的輸入之後。 RPN邏輯的簡單性是RPN電腦如此吸引程式員的一個重要原因!

繼續閱讀