第十八章:MVVM(九)

简介:

几乎是一个计算器
现在是时候使用具有Execute和CanExecute方法的ICommand对象制作更复杂的ViewModel。 下一个程序几乎就像一个计算器,只是它只添加了一系列数字。 ViewModel名为AdderViewModel,该程序名为AddingMachine。
我们先来看一下截图:
2018_10_16_103841
在页面顶部,您可以看到已输入和添加的一系列数字的历史记录。这是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计算器如此吸引程序员的一个重要原因!

目录
相关文章
|
存储 SQL 前端开发
借一个项目谈Android应用软件架构,你还在套用MVP 或MVVM吗
借一个项目谈Android应用软件架构,你还在套用MVP 或MVVM吗
|
前端开发 Java 程序员
iOS开发 - 抛开表面看本质之iOS常用架构(MVC,MVP,MVVM)
iOS开发 - 抛开表面看本质之iOS常用架构(MVC,MVP,MVVM)
471 0
|
前端开发
[译] 实用的 MVVM 和 RxSwift
今天我们将使用 RxSwift 实现 MVVM 设计模式。对于那些刚接触 RxSwift 的人,我 在这里 专门做了一个部分来介绍。
1372 0
|
前端开发 JavaScript Android开发
|
前端开发 JavaScript Android开发
|
前端开发 Android开发 Windows
|
前端开发 JavaScript Android开发
|
前端开发 JavaScript Android开发
|
JavaScript 前端开发 Android开发
|
JavaScript 前端开发 Android开发