欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

第十八章:MVVM(九)

程序员文章站 2023-12-23 19:30:45
...

几乎是一个计算器
现在是时候使用具有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计算器如此吸引程序员的一个重要原因!

上一篇:

下一篇: