从VB 开始,我已经习惯于事件驱动的编程模式了,这种模式中前端界面代码分为两部分:包含控件和布局的Form(Winform,WPF,Xamarin),和与之对应的后台代码(codebehind),后台代码包含响应前端控件的事件函数和初始化前端界面。这种方式很直接,易于理解,问题是方便进行单元测试,于是MVVM产生了。MVVM的实现仍然依赖于实现模型(这里的模型基本上是布局-后台代码),也就是完全实现MVVM需要实现模型的改进。支持WPF和Xamarin的XAML有了很大的改进,在编程模型上,支持MVVM的实现。同时,也可以按照传统的事件响应模式进行编程。在ASP.net上,对MVVM支持最好的应该是ASP.net core的RazorPage页面。
MVVM模式的介绍可以参见:
https://docs.microsoft.com/en-us/xamarin/xamarin-forms/enterprise-application-patterns/mvvm
MVVM的来历可以参见:
https://msdn.microsoft.com/en-us/magazine/dd419663.aspx
理想的MVVM实现中,View只与ViewModel打交道,不管是获取数据(通过绑定),还是执行命令(通过Command),页面的后台代码在这里应该只负责页面的初始化和绑定页面的上下文,不响应界面的事件,也不填充页面的数据。
我使用Xamarin编写了一个简单的猜五言唐诗的游戏,其中使用MVVM进行前端的设计。
前端界面代码:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:GuessPoem"
x:Class="GuessPoem.MainPage">
<StackLayout>
<!-- 显示输入的唐诗 -->
<Label Text="{Binding Path=Message}" x:Name="lbMessage"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand" FontSize="Large" />
<!-- 显示结果 -->
<Label Text="{Binding Path=Result}" x:Name="lbResult"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand" />
<!-- 显示唐诗内容 -->
<ScrollView>
<Label x:Name="lbContent" HorizontalOptions="Center" Text="{Binding Path=PoemContent}"
VerticalOptions="CenterAndExpand" />
</ScrollView>
<!-- 按钮矩阵 -->
<Grid x:Name="myGrid" >
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<!-- 显示九宫格按钮,每个按钮是一个汉字,按钮动态添加 -->
<!-- 命令按钮 -->
<Button Grid.Row="3" Grid.Column="0" x:Name="btnReset" Command="{Binding Path=CancelCommand}" Text="重选" IsEnabled="{Binding Path=ButtonEnabled}" ></Button>
<Button Grid.Row="3" Grid.Column="1" x:Name="btnSubmit" Command="{Binding Path=SubmitCommand}" Text="提交" IsEnabled="{Binding Path=ButtonEnabled}" ></Button>
<Button Grid.Row="3" Grid.Column="2" x:Name="btnNewGame" Command="{Binding Path=NewCommand}" Text="新题"></Button>
</Grid>
</StackLayout>
</ContentPage>
在后台代码中,只负责初始化和绑定VM:
namespace GuessPoem
{
public partial class MainPage : ContentPage
{
private GuessPoemViewModel viewModel;
public MainPage()
{
InitializeComponent();
viewModel = new GuessPoemViewModel(myGrid);
BindingContext = viewModel;
viewModel.NewGame();
}
}
}
View Model:
using GuessPoem.Application;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Input;
using Xamarin.Forms;
namespace GuessPoem.ViewModel
{
public class GuessPoemViewModel:INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// 新游戏命令
/// </summary>
public ICommand NewCommand { private set; get; }
/// <summary>
/// 提交命令
/// </summary>
public ICommand SubmitCommand { private set; get; }
/// <summary>
/// 撤销输入命令
/// </summary>
public ICommand CancelCommand { private set; get; }
private IGuessPoemService service;
private GuessLineDisplayDto currentGame;
private Dictionary<int, Button> buttons;
private Stack<Button> selectedButtons;
private string _Message;
private string _Result;
private string _PoemContent;
private bool _ButtonEnabled;
public GuessPoemViewModel(Grid mygrid)
{
service = new GuessPoemService();
buttons = new Dictionary<int, Button>();
int idx = 0;
for (var i = 0; i < 3; i++)
for (var j = 0; j < 3; j++)
{
var btn = new Button();
buttons.Add(idx, btn);
idx++;
mygrid.Children.Add(btn, i, j);
btn.Clicked += BtnClicked;
}
selectedButtons = new Stack<Button>();
NewCommand = new Command(() => NewGame());
SubmitCommand = new Command(() => Submit());
CancelCommand = new Command(() => Reset());
}
private void BtnClicked(object sender, EventArgs e)
{
if (selectedButtons.Count >= currentGame.TargetLine.Length) return;
var btn = sender as Button;
Message += btn.Text;
btn.IsEnabled = false;
selectedButtons.Push(btn);
}
public string Message {
get
{
return _Message;
}
set
{
if (_Message != value)
{
_Message = value;
NotifyChanged("Message");
}
}
}
public string Result {
get
{
return _Result;
}
set
{
if (_Result != value)
{
_Result = value;
NotifyChanged("Result");
}
}
}
public string PoemContent
{
get
{
return _PoemContent;
}
set
{
if (_PoemContent != value)
{
_PoemContent = value;
NotifyChanged("PoemContent");
}
}
}
public bool ButtonEnabled
{
get
{
return _ButtonEnabled;
}
set
{
_ButtonEnabled = value;
NotifyChanged("ButtonEnabled");
}
}
public virtual void NotifyChanged(params string[] propertyNames)
{
foreach (string name in propertyNames)
{
OnPropertyChanged(new PropertyChangedEventArgs(name));
}
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, e);
}
}
public void NewGame()
{
currentGame = service.NewGame();
var line = currentGame.HashLine;
for (int i = 0; i < line.Length; i++)
{
var btn = buttons[i];
btn.Text = line.Substring(i, 1);
//viewModel.GuessWords.Add(line.Substring(i, 1));
}
selectedButtons.Clear();
foreach (var btn in buttons.Values)
{
btn.IsEnabled = true;
}
Message = "";
PoemContent = "";
Result = "";
ButtonEnabled = true;
}
public void Submit()
{
var reply = new GuessReplyDto();
reply.TargetLine = currentGame.TargetLine;
reply.GuessTime = DateTime.Now;
reply.GuessReply = Message;
ButtonEnabled = false;
var res = service.Guess(reply);
var poem = service.GetPoemByLineId(int.Parse(currentGame.TargetLineId));
if (res.IsSuccess)
{
Result = "成功!";
}
else
{
Result = "错了,正确答案:“" + res.TargetLine + "”。";
}
Result += "出自《" + poem.Title + "》,作者:" + poem.Author;
PoemContent = poem.Content;
}
public void Reset()
{
if (selectedButtons.Count == 0) return;
var btn = selectedButtons.Pop();
btn.IsEnabled = true;
if (Message.Length > 0) Message = Message.Substring(0, Message.Length - 1);
}
}
}