代码改变世界

解析Caliburn.Micro(四)

2011-08-31 10:59 by 周永恒, ... 阅读, ... 评论, 收藏, 编辑

  书接前文,继续来介绍一下Caliburn.Micro(CM)中的Convention。

前言

  Caliburn.Micro这个系列也写了好几篇了,作为一个第三方的应用框架,关于细节的详尽介绍并不是第一位的,能快而准确的把握它的整体思路才是最重要的。Caliburn.Micro框架中包含了很多功能,有前面介绍过的Action,Convention,Coroutine(协同),EventAggregator(弱事件)等等。其中很多功能都是锦上添花之作,比如EventAggregator等,在Prism等框架中都有介绍,CM的核心思路是它的Action以及Convention,详细的说一下这条主线。

CM主线

  Caliburn.Micro的定位是一个轻量级的MVVM框架。作为MVVM,View和ViewModel之间用DataContext关联,View通过Binding和Command把操作传递给ViewModel并根据ViewModel中绑定属性的变化来自动刷新。用一个表格来对比一下Caliburn.Micro和通用MVVM框架的不同:

特性 普通MVVM框架 Caliburn.Micro
自动关联View和ViewModel(通过View的DataContext)。 不支持 支持,使用NameTransformer
Command 支持,推荐使用Command 不支持,使用Action作为替代方案。
Binding 支持,推荐使用Binding。 支持,可以使用Convention作为替代方案。
Validation,StringFormat,Converter,UpdateSourceTrigger 支持,使用Binding的对应机制 支持,可以使用Convention作为替代方案。

  举个简单的例子来对比:

  普通MVVM框架:

   1: <Button Command="{Binding Path=ChangeNameCommand}"/>
   2: <TextBox Text="{Binding Path=UserName}"/>

  使用Caliburn.Micro的框架

   1: <Button x:Name="ChangeName"/>
   2: <TextBox x:Name="UserName"/>

  对应的ViewModel:

Capture

  使用了Caliburn.Micro后,只需要设置Button和TextBox的Name,使它匹配到ViewModel对应属性和方法的名字,CM会自动设置绑定以及Button方法的调用。

  这个是CM设计的一个本意,使View和ViewModel之间的调用更加简单,只需要设定好对应的名字,其他的由CM来搞定。

Command与Action

  WPF中定义了Command机制,接口是ICommand,有Execute和CanExecute两个方法,并且提供了CommandParameter作为调用参数。使用Command模式,可以把执行操作的对象和处理操作的对象进行解耦,WPF这个思路是很好的,在具体实现上还存在一些不足。

  WPF中一些控件提供了Command属性,允许你加入具体操作的ICommand。Command的触发是基于Routed Event的,比如Button就是在OnClick时触发了Command的执行。WPF在很多控件上都实现了Command,比如Button等,但还不够完全,像ComboBox等控件就没有Command。

  一个Command只能响应一个Routed Event事件,对于默认的控件,比如Button等,它的响应事件是写死的(Click)。WPF在控件上提供了CommandBindings允许添加多个Command,但使用CommandBindings的写法很繁琐,对于MVVM模式的支持也不是太好。为了更好的支持MVVM模式,Blend提出了TriggerAction来更简单的添加Command,如:

   1: <Button>
   2:       <i:Interaction.Triggers>
   3:           <i:EventTrigger EventName="Click">
   4:               <i:ExecuteCommandAction TargetCommand="LoginCommand"/>
   5:           </i:EventTrigger>
   6:       </i:Interaction.Triggers>
   7: </Button>

  这里指明了Command的触发前提是Click事件发生,触发后执行对应ViewModel的LoginCommand。

  CM的Action就是基于Blend的TriggerAction,它的ActionMessage继承自TriggerAction<FrameworkElement>,不同之处在于它没有调用ViewModel中的Command,而是直接用反射拿到了对应ViewModel中的方法,去执行方法了。

Action与Convention

  为了使用简单,CM中的Action可以不指定方法的名字,它会通过控件的名字(Name)来和ViewModel中的属性、方法进行匹配。关于Action的具体使用,请参见第一篇文章。我们讨论框架的设计,这个设计不会凭空出现,设计出了ActionMessage来替代Command,我们来看看接下来会出现的问题。

  1. CM在Action的设计处采用了一种傻瓜式的操作思路,只需要你设计控件的名字(Name),其他的都用智能匹配帮你解决。既然是智能的模式,那这个智能就是控制点,控件的哪些属性需要智能使用绑定,如何保证这个智能性足够智能,使用了它并不会影响老的设计实现。
  2. WPF中的Command和Binding都是它的亮点,使用了CM,代码简洁了,Command和Binding都不见了。如何保证原有的一些功能:Command的CanExecute判断,Bindng的Validation,StringFormat,Converter等等,
  3. 复杂性与灵活性,功能强大但是代码众多的框架是让人望而生畏的,简洁明快但是缺乏扩展的框架也是不让人满意的,如何达到这样一个平衡性也是一个挑战。

  为了解决这些问题,CM引入了Convention这个概念。

ElementConvention

  关于如何自动匹配View和ViewModel,请参见前文。这里主要来介绍ElementConvention,简略来说,它是为了解决智能性而推出的,是指导具体类型Convention的处理描述。前面我们看到,使用

   1: <TextBox x:Name="UserName"/>

  CM会自动把TextBox的Text绑定到ViewModel的UserName属性上,CM在这里做的事情有二:一,它需要根据控件的类型(TextBox)来决定对于控件的哪个属性使用绑定。二,它需要根据控件的名字来做智能匹配。关于第二点,稍后再来介绍,先来看看对于不同类型的控件,CM是如何控制的。

  CM使用ElementConvention来做Convention处理的类型描述,CM提供了ConventionManager这个静态类允许操作ElementConvention。ElementConvention如下:

   1: public class ElementConvention
   2: {
   3:     public Type ElementType;
   4:     public string ParameterProperty;
   5:     public Func<DependencyObject, DependencyProperty> GetBindableProperty;
   6:     public Func<System.Windows.Interactivity.TriggerBase> CreateTrigger;
   7: }

  以TextBox为例,其中ElementType是它的类型(TextBox),ParameterProperty是作用的Property(Text),GetBindableProperty是一个Func函数,对于TextBox来说返回TextProperty,CreateTrigger返回触发事件(TextChanged)。

ConventionManager默认提供了一些类型的处理描述,如在ConventionManager的静态函数中:

   1: AddElementConvention<ButtonBase>(ButtonBase.ContentProperty, "DataContext", "Click");
   2: AddElementConvention<TextBox>(TextBox.TextProperty, "Text", "TextChanged");
   3: AddElementConvention<TextBlock>(TextBlock.TextProperty, "Text", "DataContextChanged");

  可以调用ConventionManager的AddElementConvention方法来加入新类型的处理描述,或者更改原有的ElementConvention。

Convention

  讲过了Convention的处理描述(ElementConvention),来谈一下具体的Convention过程。

  整个Convention过程是从View和ViewModel的Bind开始的:

  1. 手动或CM自动调用ViewModelBinder的Bind方法(传入对应的View和ViewModel),CM会使用View.GetApplyConventions(View)方法,来确定是否要在该View上使用Convention。如果不希望使用CM的Convention功能,可以在View上设置附加属性View.SetApplyConventions为False或者设置全局标志ViewModelBinder.ApplyConventionsByDefault为False,取消Convention。
  2. 默认是开启Convention功能的,开始进入View的Convention,遍历View的VisualTree,获得所有有名字的控件。
  3. 对这些有名字的控件使用Convention,Convention分两类,ActionConvention和PropertyConvention,Action指方法调用,Property指属性绑定。
  4. 如果使用AddElementConvention时,最后一个参数传入了触发事件Trigger的名字(Click,TextChanged)等,证明该类型控件是可以有ActionConvention的。CM会用控件的名字来匹配ViewModel中所有公共方法的名字,如果存在相同则对它使用ActionConvention。如前面的Button->ChangeName匹配ViewModel对应方法的名字,这个方法被触发的事件是AddElementConvention注册的Click事件。
       1: <Button x:Name="ChangeName" />
  5. 如果使用AddElementConvention时,第二个参数传入了属性名字,则对该类型控件使用PropertyConvention,方法与上面类似,用控件的名字匹配ViewModel中所有公共属性的名字,如果存在则设置绑定。

Validate,StringFormat,Converter…

  如果对控件使用了PropertyConvention,相当于CM自动帮我们设置了属性绑定,那么Binding里原有的一些Validate,StringFormat,Converter怎么处理?

  ConventionManager提供了SetBinding来做PropertyConvention,来看一下它的代码:

   1: public static Func<Type, string, PropertyInfo, FrameworkElement, ElementConvention, bool> SetBinding =
   2:     (viewModelType, path, property, element, convention) => {
   3:         var bindableProperty = convention.GetBindableProperty(element);
   4:         if(bindableProperty == null || HasBinding(element, bindableProperty))
   5:             return false;
   6:  
   7:         var binding = new Binding(path);
   8:  
   9:         ApplyBindingMode(binding, property);
  10:         ApplyValueConverter(binding, bindableProperty, property);
  11:         ApplyStringFormat(binding, convention, property);
  12:         ApplyValidation(binding, viewModelType, property);
  13:         ApplyUpdateSourceTrigger(bindableProperty, element, binding, property);
  14:  
  15:         BindingOperations.SetBinding(element, bindableProperty, binding);
  16:  
  17:         return true;
  18:     };

  其中的ApplyBindingMode,ApplyValueConverter等等都是Func,允许你替换原有的实现加入你自己的处理,默认的处理都非常简单。也就是说,CM帮我们自动设置的属性绑定是最基本的绑定。CM在PropertyConvention上的处理是很简单的,如果你在属性上已经手动设置了Text={Binding Path=ChangeName},那么CM会忽略这个属性。PropertyConvention只产生最基本的属性绑定,如果你希望让产生的绑定智能化你可以替换这些Func来加入你自己的处理。这也是CM整体的一个设计思路,做一些智能操作简化程序,这些操作可以开启或关闭,只做最基本的事不做多余,预留一些扩展性允许定制。

总结

  CM设计的最大成功处在于它的简洁和灵活,它可以很好的和其他一些框架合作,也可以比较方便的插入到原有的代码中。如果你已经有了自己的框架,不妨试着合并一下CM。关于CM其他的一些功能就不做更多介绍了,如有有什么疑问欢迎给我留言,也希望和朋友们多多交流,谢谢。

 

 

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。