MVP模式中Command的终极解决方案。WPF/Silverlight中的必杀技——AttachedBehavior。
(一)前言
最简单的Command,是由Prism为Silverlight提供的,执行ButtonBase的Click事件。
我在另一篇文章《MVP的前生来世》中已经使用过这个Command了。温习一下,基于MVVM实现,分为两个步骤:
1) 在ViewModel中声明这个Command,并定义该Command所对应的方法:
public DelegateCommand<RoutedEventArgs> SaveCommand = new DelegateCommand<RoutedEventArgs>(OnSave);
void OnSave(RoutedEventArgs e)
{
//do something
}
2) 在XAML中的Button中,使用Command语法,取代原先的Click事件(这里是Silverlight):
<Button Height="20" cmd:Click.Command="{Binding SaveCommand}" Content="Save"/>
更多Prism中关于Command的详细内容,请参见我的另一篇文章《Prism深入研究之Command》。
(二)WPF提供的ICommand接口
WPF中有一个ButtonBase基类,这个基类中含有一个Command属性,它是ICommand类型的,还包括一个CommandParameter属性和一个Click事件:
public abstract class ButtonBase : ContentControl, ICommandSource
{
public ICommand Command { get; set; }
public object CommandParameter { get; set; }
public event RoutedEventHandler Click;
}
public interface ICommand
{
event EventHandler CanExecuteChanged;
bool CanExecute(object parameter);
void Execute(object parameter);
}
触发Click事件会直接执行Command属性中定义的Execute方法,所以像Button(如上所示)、RadioButton、ToggleButton、CheckBox、RepeatButton和GridViewColumnHeader,这些直接或间接从这个基类派生的控件,都可以使用Command属性,而不使用事件机制。
在此,要订正一点,并不是说点击Button就不触发Click事件了,Click事件还是要触发,只是我们不再手动为Click事件添加方法了,而是把相应的逻辑添加到Command的Execute方法中。
原先的事件模型:
<Button x:Name="button1" Click="button1_Click" />
this.button1.Click += new RoutedEventHandler(this.button1_Click);
private void button1_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("After click");
}
现在的Command模型:
<Button Command="{Binding}" />
this.button.DataContext = ClickCommand;
public DelegateCommand<object> ClickCommand { get; private set; }
ClickCommand = new DelegateCommand<object>(OnClick, arg => true);
void OnClick(object e)
{
MessageBox.Show("After click");
}
信手写了个Demo,以比较二上述两种编程方式的不同,代码下载:WpfApplication10.zip
(二)Silverlight下ButtonBase的Click实现
在Silverlight中,虽然也有ICommand接口,但是并未在ButtonBase基类中提供ICommand类型的Command属性,所以不能使用上述机制来“化Event为Command”。
直到有一天,一位微软MVP提出了AttachedBehavior的思想,才彻底解决了这个问题。
AttachedBehavior定义:把由控件触发的事件和Presenter(或ViewModel)中的代码相关联。
AttachedBehavior由两部分组成:一个attached 属性和一个behavior对象。attached 属性建立目标控件和behavior对象之间的关系;behavior对象则监视着目标控件,当目标控件的状态改变或事件触发时,就执行一些操作,我们可以在这里写一些自己的逻辑。
微软P & P Team根据这一思想,在Prism中成功地将ButtonBase的Click事件转换为了Command。
实现过程如下:
1)创建CommandBehaviorBase<T>泛型类,可以将其看作Behavior基类:
public class CommandBehaviorBase<T>
where T: Control
这里T继承自Control基类,我们后面会看到这样设置的灵活性。
类图如下所示:
我们看到,CommandBehaviorBase<T>中有3个属性,看着眼熟,这不由使我们想起了WPF中ButtonBase的定义:
public abstract class ButtonBase
{
public ICommand Command { get; set; }
public object CommandParameter { get; set; }
public IInputElement CommandTarget { get; set; }
//省略一些成员
}
就是说,我们把这3个属性从ButtonBase提升到了Control级别,从而可以让这个CommandBehaviorBase<T>泛型类适用于绝大多数控件(这里没有说全部哦,有一些特例我会在下文介绍)。
TargetObject会在构造函数中初始化,这个属性就是目标控件了。
CommandBehaviorBase<T>暴露了一个ExecuteCommand方法,可以在它的派生类中调用甚至重写该方法,以执行相应的Command:
protected virtual void ExecuteCommand()
{
if (this.Command != null)
{
this.Command.Execute(this.CommandParameter);
}
}
CommandBehaviorBase<T>的UpdateEnabledState方法是比较有趣的,在设置Command和CommandParameter属性的时候,都会调用该方法,从而根据Command的CanExecute方法返回true/false,决定目标控件的IsEnabled属性。
2)定义ButtonBaseClickCommandBehavior类,这时一个具体的Behavior,所以它派生自CommandBehaviorBase<ButtonBase>。类的定义及其关系图如下所示:
public class ButtonBaseClickCommandBehavior : CommandBehaviorBase<ButtonBase>
{
public ButtonBaseClickCommandBehavior(ButtonBase clickableObject) : base(clickableObject)
{
clickableObject.Click += OnClick;
}
private void OnClick(object sender, System.Windows.RoutedEventArgs e)
{
ExecuteCommand();
}
}
这个类很简单,在它的构造函数中传递ButtonBase目标控件,并把OnClick方法绑到这个目标控件的Click事件上。
至此,我们创建了一个Behavior,它是为ButtonBase的Click事件量身打造的。
3)最后我们来创建attached属性,并建立目标控件和behavior对象之间的关系。
为此,需要为ButtonBase的Click事件单独创建一个Click类。这个Click类是用于在XAML绑定Command的:
public static class Click
我们要在类中注册一个依赖属性(Dependency Property,简称DP)ClickCommandBehavior,用来将之前创建的ButtonBaseClickCommandBehavior永久性保存在类中,而GetOrCreateBehavior方法则用来得到这个behavior:
public static class Click
{
private static readonly DependencyProperty ClickCommandBehaviorProperty = DependencyProperty.RegisterAttached(
"ClickCommandBehavior",
typeof(ButtonBaseClickCommandBehavior),
typeof(Click),
null);
private static ButtonBaseClickCommandBehavior GetOrCreateBehavior(ButtonBase buttonBase)
{
ButtonBaseClickCommandBehavior behavior = buttonBase.GetValue(ClickCommandBehaviorProperty) as ButtonBaseClickCommandBehavior;
if (behavior == null)
{
behavior = new ButtonBaseClickCommandBehavior(buttonBase);
buttonBase.SetValue(ClickCommandBehaviorProperty, behavior);
}
return behavior;
}
}
为了能写出下面这样的绑定语法,Click必须是静态的(static),其中cmd是对Click所在namespace的引用:
<Button cmd:Click.Command="{Binding SaveCommand}" cmd:Click.CommandParameter="BaoBao" />
我们要在Click类中添加两个属性Command和CommandParameter,而且为了实现持久性绑定,要把它们设计成DP:
public static readonly DependencyProperty CommandProperty = DependencyProperty.RegisterAttached(
"Command",
typeof(ICommand),
typeof(Click),
new PropertyMetadata(OnSetCommandCallback));
public static readonly DependencyProperty CommandParameterProperty = DependencyProperty.RegisterAttached(
"CommandParameter",
typeof(object),
typeof(Click),
new PropertyMetadata(OnSetCommandParameterCallback));
我们分别为这两个DP设计了回调方法:
private static void OnSetCommandCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
ButtonBase buttonBase = dependencyObject as ButtonBase;
if (buttonBase != null)
{
ButtonBaseClickCommandBehavior behavior = GetOrCreateBehavior(buttonBase);
behavior.Command = e.NewValue as ICommand;
}
}
private static void OnSetCommandParameterCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
ButtonBase buttonBase = dependencyObject as ButtonBase;
if (buttonBase != null)
{
ButtonBaseClickCommandBehavior behavior = GetOrCreateBehavior(buttonBase);
behavior.CommandParameter = e.NewValue;
}
}
它们分别调用GetOrCreateBehavior方法,以获取永久性存储在类中的behavior实例,并将在XAML中设置的Command和CommandParameter保存在behavior实例中。
至此,一个AttachedBehavior设计完毕。
Prism提供的实现位于Composite.Presentation.Desktop这个项目中,如图所示:
(三)对Click的重构
Prism为我们提供的ButtonBase的Click实现,是一个很不错的参考,为我们提供了AttachedBehavior的编程模型。
但是,根据我的经验,CommandParameter一般是不会使用的,因为我们在实际编程中,是有机会在Presenter(或ViewModel)中把Command所需要的参数传递到要执行的OnExecute方法的。所以,我尝试着简化Click的编程模型,就是把和CommandParameter、TargetObject有关的逻辑全都删除:
CommandBehaviorBase类修改如下:
public class CommandBehaviorBase<T>
where T: Control
{
private ICommand command;
public ICommand Command
{
get { return command; }
set
{
this.command = value;
}
}
protected virtual void ExecuteCommand()
{
if (this.Command != null)
{
this.Command.Execute(null);
}
}
}
Click类修改如下:
public static class Click
{
private static readonly DependencyProperty ClickCommandBehaviorProperty = DependencyProperty.RegisterAttached(
"ClickCommandBehavior",
typeof(ButtonBaseClickCommandBehavior),
typeof(Click),
null);
public static readonly DependencyProperty CommandProperty = DependencyProperty.RegisterAttached(
"Command",
typeof(ICommand),
typeof(Click),
new PropertyMetadata(OnSetCommandCallback));
public static void SetCommand(ButtonBase buttonBase, ICommand command)
{
buttonBase.SetValue(CommandProperty, command);
}
public static ICommand GetCommand(ButtonBase buttonBase)
{
return buttonBase.GetValue(CommandProperty) as ICommand;
}
private static void OnSetCommandCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
ButtonBase buttonBase = dependencyObject as ButtonBase;
if (buttonBase != null)
{
ButtonBaseClickCommandBehavior behavior = GetOrCreateBehavior(buttonBase);
behavior.Command = e.NewValue as ICommand;
}
}
private static ButtonBaseClickCommandBehavior GetOrCreateBehavior(ButtonBase buttonBase)
{
ButtonBaseClickCommandBehavior behavior = buttonBase.GetValue(ClickCommandBehaviorProperty) as ButtonBaseClickCommandBehavior;
if (behavior == null)
{
behavior = new ButtonBaseClickCommandBehavior(buttonBase);
buttonBase.SetValue(ClickCommandBehaviorProperty, behavior);
}
return behavior;
}
}
ButtonBaseClickCommandBehavior类修改如下:
public class ButtonBaseClickCommandBehavior : CommandBehaviorBase<ButtonBase>
{
public ButtonBaseClickCommandBehavior(ButtonBase clickableObject)
{
clickableObject.Click += OnClick;
}
private void OnClick(object sender, System.Windows.RoutedEventArgs e)
{
ExecuteCommand();
}
}
精简之后的逻辑,是不是很清晰了呢?所以,我们在编写自己的Command时,功能够用就好,别搞得太复杂。
我在此基础上写了一个示例,运行良好。示例下载:SLPrismApplicationForClickCommand.zip
(四)定义自己的Command
说了这么多,无非是为编写我们自己的Command做准备,不光是Silverlight要用到AttachedBehavior,就连WPF也照用不误,因为WPF只在ButtonBase中实现了Command,而并没有在其他控件中建立类似的机制。
授人以渔,不如授人以渔。看到有朋友在搜集各种AttachedBehavior以备不时之需,其实没有必要,完全可以随心所欲进行定制。下面将介绍5种自定义AttachedBehavior,都是我在项目中所遇到的,基本涵盖了所有类型的控件事件。
(五)ButtonBase的MouseOver事件
其实很简单,只要把上面Click那个AttachedBehavior中相应的Click事件修改为MouseOver事件就可以了,然后当你把鼠标移动到Button,就会执行相应的命令。
第一次重构后的代码下载,为了代码可读性更强,我还把带有Click的名字都改为了MouseOver:SLAttachedBehavior_1.zip
但是,仔细观察新的代码,我们发现,它只适用于ButtonBase。而MouseOver事件是在UIELement这个类中定义的,类的关系如图所示:
所以,我们可以把MouseOver的AttachedBehavior的范围做的更广泛一些——适用于所有UIELement。
修改CommandBehaviorBase<T>,让T的约束范围限制为DependencyObject:
public class CommandBehaviorBase<T>
where T : DependencyObject
修改UIElementMouseMoveCommandBehavior,使其派生于CommandBehaviorBase<UIElement>,并修改其构造函数,即将ButtonBase修改为UIElement:
public class UIElementMouseMoveCommandBehavior : CommandBehaviorBase<UIElement>
{
public UIElementMouseMoveCommandBehavior(UIElement obj)
{
obj.MouseMove += OnMouseMove;
}
private void OnMouseMove(object sender, System.Windows.RoutedEventArgs e)
{
ExecuteCommand();
}
}
修改MouseMove类,将ButtonBase修改为UIElement。
示例代码下载如下:SLAttachedBehavior_2.zip
一切工作良好。至此,我们得出一个结论:对一个事件,先找到它是由哪个类中提供的,然后将CommandBehaviorBase<T>中的T相应替换为这个类,其它地方照猫画虎即可。
(六)TextBox的TextChanged事件
有了前面的指导思想,这个例子实现起来就简单了。
TextBox的TextChanged事件是定义在TextBox中生的,是TextBox所独有的,没有通用性,所以要将CommandBehaviorBase<T>中的T替换为TextBox:
public class TextBoxTextChangedCommandBehavior : CommandBehaviorBase<TextBox>
{
public TextBoxTextChangedCommandBehavior(TextBox obj)
{
obj.TextChanged += OnTextChanged;
}
private void OnTextChanged(object sender, TextChangedEventArgs e)
{
ExecuteCommand();
}
}
TextBox是从Control派生的,所以将CommandBehaviorBase<T>的定义修改为:
public class CommandBehaviorBase<T>
where T : Control
接下来定义静态类TextChanged,只要把上面那个例子中MouseMove类的MouseMove全都替换为TextChanged,把UIElement全都替换为TextBox即可。
示例代码下载:SLAttachedBehavior_3.zip
这个AttachedBehavior在《Command探究》一文中 Dirty Save示例中会使用到。
(七)TextBlock的MouseLeftButtonUp事件
TextBlock控件没有Click事件,但是可以用MouseLeftButtonUp事件来模拟。
如果是单独一个TextBlock的MouseLeftButtonUp事件,按照上面的编码模式去套,是很容易的。
沿着TextBlock的继承关系一直向上找,直到在UIElement基类中发现MouseLeftButtonUp事件。
于是创建适用于所有UIElement的MouseLeftButtonUpCommandBehavior,
public class CommandBehaviorBase<T> { }
public class MouseLeftButtonUpCommandBehavior : CommandBehaviorBase<UIElement>
{
public MouseLeftButtonUpCommandBehavior(UIElement clickableObject)
{
clickableObject.MouseLeftButtonUp += OnMouseLeftButtonUp;
}
private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
ExecuteCommand();
}
}
然后仿照上面例子中的Click类,创建一个MouseLeftButtonUp类,实现很简单,把Click类中的Click全都替换为MouseLeftButtonUp,把ButtonBase全都替换为UIElement即可:
public static class MouseLeftButtonUp
{
private static readonly DependencyProperty MouseLeftButtonUpCommandBehaviorProperty = DependencyProperty.RegisterAttached(
"MouseLeftButtonUpCommandBehavior",
typeof(MouseLeftButtonUpCommandBehavior),
typeof(MouseLeftButtonUp),
null);
public static readonly DependencyProperty CommandProperty = DependencyProperty.RegisterAttached(
"Command",
typeof(ICommand),
typeof(MouseLeftButtonUp),
new PropertyMetadata(OnSetCommandCallback));
public static void SetCommand(UIElement element, ICommand command)
{
element.SetValue(CommandProperty, command);
}
public static ICommand GetCommand(UIElement element)
{
return element.GetValue(CommandProperty) as ICommand;
}
private static void OnSetCommandCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
UIElement element = dependencyObject as UIElement;
if (element != null)
{
MouseLeftButtonUpCommandBehavior behavior = GetOrCreateBehavior(element);
behavior.Command = e.NewValue as ICommand;
}
}
private static MouseLeftButtonUpCommandBehavior GetOrCreateBehavior(UIElement element)
{
MouseLeftButtonUpCommandBehavior behavior = element.GetValue(MouseLeftButtonUpCommandBehaviorProperty) as MouseLeftButtonUpCommandBehavior;
if (behavior == null)
{
behavior = new MouseLeftButtonUpCommandBehavior(element);
element.SetValue(MouseLeftButtonUpCommandBehaviorProperty, behavior);
}
return behavior;
}
}
示例代码下载:WpfApplication3.zip
这个例子只是让大家熟悉如何自定义AttachedBehavior,相信大家已经掌握了,下面我们研究一些高级货。
(八)GridView中TextBlock的MouseLeftButtonUp事件
单个TextBlock的MouseLeftButtonUp事件是很容易实现的。但是,在GridView的模板列中的TextBlock的MouseLeftButtonUp事件,就比较难以实现了。
如果我们还像过去一样编码,在模板列的TextBlock中绑定这个Command:
<data:DataGridTemplateColumn Header="UserName" >
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=UserName}" cmd:MouseLeftButtonUp.Command="{Binding MouseLeftButtonUpCommand}" />
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
</data:DataGridTemplateColumn>
但是,点击DataGrid中每一行的UserName列,却发现没有任何反应,就是说,Command失效了。
错误代码下载:WpfApplication7_error.zip
怎么办呢?
有一个馊主意,既然TextBlock是在DataGrid中的,而且DataGrid也具有MouseLeftButtonUp事件,所以不妨在DataGrid添加这个Command,如下所示:
<data:DataGrid AutoGenerateColumns="False" Margin="12,12,66,0" Name="dataGrid1"
ItemsSource="{Binding StudentList, Mode=TwoWay}" Height="140" VerticalAlignment="Top"
cmd:MouseLeftButtonUp.Command="{Binding MouseLeftButtonUpCommand}"
>
貌似运行良好哦。
示例代码下载:WpfApplication7_error2.zip
但是,大家想过没有,这是治标不治本的办法,如果DataGrid中有多个TextBlock,那该如何分辨是哪个TextBlock触发了MouseLeftButtonUp事件?
此外,在DataGrid中,不光是TextBlock的MouseLeftButtonUp事件失效,我们将其换成Button的Click事件,会发现也不行。
<data:DataGridTemplateColumn Header="Score">
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="{Binding Path=Score}" Command="{Binding Path=MouseLeftButtonUpCommand" />
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
</data:DataGridTemplateColumn>
这次就不能把Button的Click事件放到DataGrid上了,因为DataGrid可是没有这个Click的哦?
为什么会这样呢?
在于我们的绑定语法写的不对。
1)先来看一下WPF。像DataView、ListView这样的数据集合控件,对于其中的TextBlock、Button这样的控件,它们的Command绑定语法应该写成这样:
<data:DataGridTemplateColumn Header="UserName" >
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=UserName}" local:MouseLeftButtonUp.Command="{Binding Path=DataContext.MouseLeftButtonUpCommand, RelativeSource={RelativeSource AncestorType={x:Type data:DataGrid}}}" />
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
</data:DataGridTemplateColumn>
就是说,在WPF中,使用到了RelativeResource,将DataGrid中的TextBlock控件的数据源指到外面去(跳出三界外,不在五行中),此时路径要指向DataGrid的DataContext的MouseLeftButtonUpCommand。
对于Button,也是如法炮制:
<data:DataGridTemplateColumn Header="Score">
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Content="Buy" Command="{Binding Path=DataContext.BuyCommand, RelativeSource={RelativeSource AncestorType={x:Type data:DataGrid}}}" Width="30" Height="20" Cursor="Hand" />
<Button Content="Sell" Command="{Binding Path=DataContext.SellCommand, RelativeSource={RelativeSource AncestorType={x:Type data:DataGrid}}}" Width="30" Height="20" Cursor="Hand" />
</StackPanel>
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
</data:DataGridTemplateColumn>
示例代码下载:WpfApplication8.zip
2)再来看一下Silverlight。
由于Silverlight不支持RelativeResource语法,所以我们要另想别的办法。
还记得XAML中有一种Resource语法么,我们可以把Command存在Resource中,相当于建立了一个全局变量,让DataGrid中的TextBlock和Button直接指向这个Resource(有点儿直辖市的味道哦)。
能够存储Command的Resource,要额外花些心思来构思,为此要创建一个名为ObservableCommand的类,以后存储Command就靠它了:
public class ObservableCommand : ObservableObject<ICommand> { }
在Xaml中,我们建立3个资源:
<UserControl.Resources>
<local:ObservableCommand x:Key="BuyCommand" />
<local:ObservableCommand x:Key="SellCommand" />
<local:ObservableCommand x:Key="MouseLeftButtonUpCommand" />
</UserControl.Resources>
并在后台的代码文件中,在ViewModel的set属性中,手动把它们和ViewModel的Command绑定在一起:
public partial class ScoreListView : UserControl
{
public ScoreListView()
{
InitializeComponent();
this.ViewModel = new ScoreListViewModel();
this.ViewModel.View = this;
}
public ScoreListViewModel ViewModel
{
get
{
return this.DataContext as ScoreListViewModel;
}
set
{
this.DataContext = value;
((ObservableCommand)this.Resources["BuyCommand"]).Value = value.BuyCommand;
((ObservableCommand)this.Resources["SellCommand"]).Value = value.SellCommand;
((ObservableCommand)this.Resources["MouseLeftButtonUpCommand"]).Value = value.MouseLeftButtonUpCommand;
}
}
}
这样,我们就可以在DataGrid的TextBlock和Button中把它们绑定到资源上了:
<data:DataGrid AutoGenerateColumns="False" Margin="12" Name="dataGrid1" ItemsSource="{Binding StudentList}">
<data:DataGrid.Columns>
<data:DataGridTemplateColumn Header="UserName" >
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<TextBlock Text="{Binding Path=UserName}" local:MouseLeftButtonUp.Command="{Binding Path=Value, Source={StaticResource MouseLeftButtonUpCommand}}" />
</StackPanel>
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
</data:DataGridTemplateColumn>
<data:DataGridTemplateColumn Header="Score">
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Commands:Click.Command="{Binding Path=Value, Source={StaticResource BuyCommand}}" Width="30" Height="20" Cursor="Hand" Content="Buy" />
<Button Commands:Click.Command="{Binding Path=Value, Source={StaticResource SellCommand}}" Width="30" Height="20" Cursor="Hand" Content="Sell" />
</StackPanel>
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
</data:DataGridTemplateColumn>
</data:DataGrid.Columns>
</data:DataGrid>
注意绑定语法:
<Button Commands:Click.Command="{Binding Path=Value, Source={StaticResource BuyCommand}}" />
这里绑定的是BuyCommand资源的Value值。
示例代码下载:SilverlightApplication12.zip
(九)Popup Window的弹出和关闭事件
这个例子是由Prism的StockTrader RI提供的,堪称绝世经典之作。
关于这个功能的介绍,请参见《Prism研究RI分析 之六 PopupView》。别写到RI分析的时候没得写了。
(十)结语
AttachedBehavior是好东西啊!虽然有点神秘,但却不是那么费解。希望大家仔细阅读此文,多研究我提供的源码。我尽量把示例做的简单,而没有使用太多的Prism框架。