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