用了三年多的WPF,开发了很多个WPF的项目,就我自己的经验,谈一谈如何学好WPF,当然,抛砖引玉,如果您有什么建议也希望不吝赐教。

  WPF,全名是Windows Presentation Foundation,是微软在.net3.0 WinFX中提出的。WPF是对Direct3D的托管封装,它的图形表现依赖于显卡。当然,作为一种更高层次的封装,对于硬件本身不支持的一些图形特效的硬实现,WPF提供了利用CPU进行计算的软实现,用以简化开发人员的工作。

  简单的介绍了一下WPF,这方面的资料也有很多。作于微软力推的技术,整个推行也符合微软一贯的风格。简单,易用,强大,外加几个创新概念的噱头。

  噱头一:声明式编程。从理论上讲,这个不算什么创新。Web界面声明式开发早已如火如荼了,这种界面层的声明式开发也是大势所趋。为了适应声明式编程,微软推出了XAML,一种扩展的XML语言,并且在.NET 3.0中加入了XAML的编译器和运行时解析器。XAML加上IDE强大的智能感知,确实大大方便了界面的描述,这点是值得肯定的。

  噱头二:紧接着,微软借XAML描绘了一副更为美好的图片,界面设计者和代码开发者可以并行的工作,两者通过XAML进行交互,实现设计和实现的分离。不得不说,这个想法非常打动人心。以往设计人员大多是通过photoshop编辑出来的图片来和开发人员进行交互的,需要开发人员根据图片的样式来进行转换,以生成实际的效果。既然有了这层转换,所以最终出来的效果和设计时总会有偏差,所以很多时候开发人员不得不忍受设计人员的抱怨。WPF的出现给开发人员看到了一线曙光,我只负责逻辑代码,UI你自己去搞,一结合就可以了,不错。可实际开发中,这里又出现了问题,UI的XAML部分能完全丢给设计人员么?

  这个话题展开可能有点长,微软提供了Expression Studio套装来支持用工具生成XAML。那么这套工具是否能够满足设计人员的需要呢?经过和很多设计人员和开发人员的配合,最常听到的话类似于这样。“这个没有Photoshop好用,会限制我的灵感”, “他们生成的XAML太糟糕了…”。确实,在同一项目中,设计人员使用Blend进行设计,开发人员用VS来开发代码逻辑,这个想法稍有理想化:   · 有些UI效果是很难或者不可以用XAML来描述的,需要手动编写效果。   · 大多数设计人员很难接受面向对象思维,包括对资源(Resource)的复用也不理想   · 用Blend生成的XAML代码并不高效,一种很简单的布局也可能被翻译成很冗长的XAML。

  在经历过这样不愉快的配合后,很多公司引入了一个integrator的概念。专门抽出一个比较有经验的开发人员,负责把设计人员提供的XAML代码整理成比较符合要求的XAML,并且在设计人员无法实现XAML的情况下,根据设计人员的需要来编写XAML或者手动编写代码。关于这方面,我的经验是,设计人员放弃Blend,使用Expression Design。Design工具还是比较符合设计人员的思维,当然,需要特别注意一些像素对齐方面的小问题。开发人员再通过设计人员提供的design文件转化到项目中。这里一般是用Blend打开工程,Expression系列复制粘贴是格式化到剪切板的,所以可以在design文件中选中某一个图形,点复制,再切到blend对应的父节点下点粘贴,适当修改一下转化过来的效果。

  作为一个矢量化图形工具,Expression Studio确实给我们提供了很多帮助,也可以达到设计人员同开发人员进行合作,不过,不像微软描述的那样自然。总的来说,还好,但是不够好。

  这里,要步入本篇文章的重点了,也是我很多时候听起来很无奈的事情。微软在宣传WPF时过于宣传XAML和工具的简易性了,造成很多刚接触WPF的朋友们会产生这样一副想法。WPF=XAML? 哦,类似HTML的玩意…

  这个是不对的,或者是不能这么说的。作为一款新的图形引擎,以Foundation作为后缀,代表了微软的野心。借助于托管平台的支持,微软寄希望WPF打破长久以来桌面开发和Web开发的壁垒。当然,由于需要.net3.0+版本的支持,XBAP已经逐渐被Silverlight所取替。在整个WPF的设计里,XAML(Markup)确实是他的亮点,也吸取了Web开发的精华。XAML对于帮助UI和实现的分离,有如如虎添翼。但XAML并不是WPF独有的,包括WF等其他技术也在使用它,如果你愿意,所有的UI你也可以完成用后台代码来实现。正是为了说明这个概念,Petzold在Application = codes + markup 一书中一分为二,前半本书完全使用Code来实现的,后面才讲解了XAML以及在XAML中声明UI等。但这本书叫好不叫座,你有一定开发经验回头来看发现条条是路,非常经典,但你抱着这本书入门的话估计你可能就会一头雾水了。

  所以很多朋友来抱怨,WPF的学习太曲折了,上手很容易,可是深入一些就比较困难,经常碰到一些诡异困难的问题,最后只能推到不能做,不支持。复杂是由数量级别决定的,这里借LearnWPF的一些数据,来对比一下Asp.net, WinForm和WPF 类型以及类的数量:

ASP.NET 2.0 WinForms 2.0 WPF
c3d.club c3d.club c3d.club
1098 public types1551 classes 777 public types1500 classes 1577 public types3592 classes

  当然,这个数字未必准确,也不能由此说明WPF相比Asp.net、WinForm,有多复杂。但是面对如此庞大的类库,想要做到一览众山小也是很困难的。想要搞定一个大家伙,我们就要把握它的脉络,所谓庖丁解牛,也需要知道在哪下刀。在正式谈如何学好WPF之前,我想和朋友们谈一下如何学好一门新技术。

  学习新技术有很多种途经,自学,培训等等。相对于我们来说,听说一门新技术,引起我们的兴趣,查询相关讲解的书籍(资料),边看书边动手写写Sample这种方式应该算最常见的。那么怎么样才算学好了,怎么样才算是学会了呢?在这里,解释下知识树的概念:

c3d.club

  这不是什么创造性的概念,也不想就此谈大。我感觉学习主要是两方面的事情,一方面是向内,一方面是向外。这棵所谓树的底层就是一些基础,当然,只是个举例,具体图中是不是这样的逻辑就不要见怪了。学习,就是一个不断丰富自己知识树的过程,我们一方面在努力的学习新东西,为它添枝加叶;另一方面,也会不停的思考,理清脉络。这里谈一下向内的概念,并不是没有学会底层一些的东西,上面的东西就全是空中楼阁了。很少有一门技术是仅仅从一方面发展来的,就是说它肯定不是只有一个根的。比方说没有学过IL,我并不认为.NET就无法学好,你可以从另外一个根,从相对高一些的抽象上来理解它。但是对底层,对这种关键的根,学一学它还是有助于我们理解的。这里我的感觉是,向内的探索是无止境的,向外的扩展是无限可能的。

  介绍了这个,接下来细谈一下如何学好一门新技术,也就是如何添砖加瓦。学习一门技术,就像新new了一个对象,你对它有了个大致了解,但它是游离在你的知识树之外的,你要做的很重要的一步就是把它连好。当然这层向内的连接不是一夕之功,可能会连错,可能会少连。我对学好的理解是要从外到内,再从内到外,就读书的例子谈一下这个过程:

  市面关于技术的书很多,名字也五花八门的,简单的整理一下,分为三类,就叫V1,V2,V3吧。 · V1类,名字一般比较好认,类似30天学通XXX,一步一步XXX…没错,入门类书。这种书大致上都是以展示为主的,一个一个Sample,一步一步的带你过一下整个技术。大多数我们学习也都是从这开始的,倒杯茶水,打开电子书,再打开VS,敲敲代码,只要注意力集中些,基本不会跟不上。学完这一步,你应该对这门技术有了一定的了解,当然,你脑海中肯定不自觉的为这个向内连了很多线,当然不一定正确,不过这个新东东的创建已经有轮廓了,我们说,已经达到了从外的目的。 · V2类,名字就比较乱了,其实意思差不多,只是用的词语不一样。这类有深入解析XXX,XXX本质论…这种书良莠不齐,有些明明是入门类书非要换个马甲。这类书主要是详细的讲一下书中的各个Feature, 来龙去脉,帮你更好的认识这门技术。如果你是带着问题去的,大多数也会帮你理清,书中也会顺带提一下这个技术的来源,帮你更好的把请脉络。这种书是可以看出作者的功力的,是不是真正达到了深入浅出。这个过程结束,我们说,已经达到了从外到内的目的。 · V3类,如果你认真,踏实的走过了前两个阶段,我觉得在简历上写个精通也不为过。这里提到V3,其实名字上和V2也差不多。往内走的越深,越有种冲动想把这东西搞透,就像被强行注入了内力,虽然和体内脉络已经和谐了,不过总该自己试试怎么流转吧。这里谈到的就是由内向外的过程,第一本给我留下深刻印象的书就是侯捷老师的深入浅出MFC,在这本书中,侯捷老师从零开始,一步一步的构建起了整个类MFC的框架结构。书读两遍,如醍醐灌顶,痛快淋漓。如果朋友们有这种有思想,讲思想,有匠心的书也希望多多推荐,共同进步。

  回过头,就这个说一下WPF。WPF现在的书也有不少,入门的书我首推MSDN。其实我觉得作为入门类的书,微软的介绍就已经很好了,面面俱到,用词准确,Sample附带的也不错。再往下走,比如Sams.Windows.Presentation.Foundation.Unleashed或者Apress_Pro_WPF_Windows_Presentation_Foundation_in_NET_3_0也都非常不错。这里没有看到太深入的文章,偶有深入的也都是一笔带过,或者是直接用Reflector展示一下Code。

  接下来,谈一下WPF的一些Feature。因为工作关系,经常要给同事们培训讲解WPF,越来越发现,学好学懂未必能讲懂讲透,慢慢才体会到,这是一个插入点的问题。大家的水平参差不齐,也就是所谓的总口难调,那么讲解的插入点就决定了这个讲解能否有一个好的效果,这个插入点一定要尽可能多的插入到大家的知识树上去。最开始的插入点是大家比较熟悉的部分,那么往后的讲解就能一气通贯,反之就是一个接一个的概念,也就是最讨厌的用概念讲概念,搞得人一头雾水。

  首先说一下Dependency Property(DP)。这个也有很多人讲过了,包括我也经常和人讲起。讲它的储存,属性的继承,验证和强制值,反射和值储存的优先级等。那么为什么要有DP,它能带来什么好处呢?

  抛开DP,先说一下Property,属性,用来封装类的数据的。那么DP,翻译过来是依赖属性,也就是说类的属性要存在依赖,那么这层依赖是怎么来的呢。任意的一个DP,MSDN上的一个实践是这样的: gfv

public static readonly DependencyProperty IsSpinningProperty = DependencyProperty.Register("IsSpinning", typeof(bool));

public bool IsSpinning
{
    get { return (bool)GetValue(IsSpinningProperty); }
    set { SetValue(IsSpinningProperty, value); }
}

  单看IsSpinning,和传统的属性没什么区别,类型是bool型,有get,set方法。只不过内部的实现分别调用了GetValue和SetValue,这两个方法是DependecyObject(简称DO,是WPF中所有可视Visual的基类)暴露出来的,传入的参数是IsSpinningProperty。再看IsSpinningProperty,类型就是DependencyProperty,前面用了static readonly,一个单例模式,有DependencyProperty.Register,看起来像是往容器里注册。

  粗略一看,也就是如此。那么,它真正的创新、威力在哪里呢。抛开它精巧的设计不说,先看储存。DP中的数据也是存储在对象中的,每个DependencyObject内部维护了一个EffectiveValueEntry的数组,这个EffectiveValueEntry是一个结构,封装了一个DependencyProerty的各个状态值animatedValue(作动画),baseValue(原始值),coercedValue(强制值),expressionValue(表达式值)。我们使用DenpendencyObject.GetValue(IsSpinningProperty)时,就首先取到了该DP对应的EffectiveValueEntry,然后返回当前的Value。

  那么,它和传统属性的区别在哪里,为什么要搞出这样一个DP呢?第一,内存使用量。我们设计控件,不可避免的要设计很多控件的属性,高度,宽度等等,这样就会有大量(私有)字段的存在,一个继承树下来,低端的对象会无法避免的膨胀。而外部通过GetValue,SetValue暴露属性,内部维护这样一个EffectiveValueEntry的数组,顾名思义,只是维护了一个有效的、设置过值的列表,可以减少内存的使用量。第二,传统属性的局限性,这个有很多,包括一个属性只能设置一个值,不能得到变化的通知,无法为现有的类添加新的属性等等。

  这里谈谈DP的动态性,表现有二:可以为类A设置类B的属性;可以给类A添加新的属性。这都是传统属性所不具备的,那么是什么让DependencyObject具有这样的能力呢,就是这个DenpencyProperty的设计。在DP的设计中,对于单个的DP来说,是单例模式,也就是构造函数私有,我们调用DependencyProperty.Register或者DependencyProperty.RegisterAttached这些静态函数的时候,内部就会调用到私有的DP 的构造函数,构建出新的DP,并把这个DP加入到全局静态的一个HashTable中,键值就是根据传入时的名字和对象类型的hashcode取异或生成的。

  既然DependencyProperty是维护在一个全局的HashTable中的,那么具体到每个对象的属性又是怎么通过GetValue和SetValue来和DependencyProperty关联上的,并获得PropertyChangeCallback等等的能力呢。在一个DP的注册方法中,最多传递五个参数 :

public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata,ValidateValueCallback validateValueCallback);

其中第一和第三个参数就是用来确定HashTable中的键值,第二个参数确定了属性的类型,第四个参数是DP中的重点,定义了DP的属性元数据。在元数据中,定义了属性变化和强制值的Callback等。那么在一个SetValue的过程中,会出现哪些步骤呢:

  1. 取得该DP下对应这个DependencyObject的PropertyMetadata,这句可能听起来有些拗口。Metadata,按微软一般的命名规则,一般是用来描述对象自身数据的,那么一个DP是否只含有一个propertyMetadata呢?答案是不是,一个DP内部维护了一个比较高效的map,里面存储了多个propertyMetadata,也就是说DP和propertyMetadata是一对多的关系。这里是为什么呢,因为同一个DP可能会被用到不同的DependencyObject中去,对于每类DependencyObject,对这个DP的处理都有所不同,这个不同可以表现在默认值不同,properyMetadata里面的功能不同等等,所以在设计DP的时候设计了这样一个DP和propertyMetadata一对多的关系。
  2. 取得了该DP下对应真正干活的PropertyMetadata,下一步要真正的”SetValue”了。这个“value”就是要设置的值,设置之后要保存到我们前面提到的EffectiveValueEntry上,所以这里还要先取得这个DP对应的EffectiveValueEntry。在DependencyObject内部的EffectiveValueEntry的数组里面查找这个EffectiveValueEntry,有,取得;没有,新建,加入到数组中。
  3. 那么这个EffectiveValueEntry到底是用来干什么的,为什么需要这样一个结构体?如果你对WPF有一定了解,可能会听说WPF值储存的优先级,local value>style trigger>template trigger>…。在一个EffectiveValueEntry中,定义了一个BaseValueSourceInternal,用来表示设置当前Value的优先级,当你用新的EffectiveValueEntry更新原有的EffectiveValueEntry时,如果新的EffectiveValueEntry中BaseValueSourceInternal高于老的,设置成功,否则,不予设置。
  4. 剩下的就是proertyMetadata了,当你使用类似如下的参数注册DP,
public static readonly DependencyProperty CurrentReadingProperty = DependencyProperty.Register(
    "CurrentReading",
    typeof(double),
    typeof(Gauge),
    new FrameworkPropertyMetadata(
        Double.NaN,
        FrameworkPropertyMetadataOptions.AffectsMeasure,
        new PropertyChangedCallback(OnCurrentReadingChanged),
        new CoerceValueCallback(CoerceCurrentReading)), new ValidateValueCallback(IsValidReading));

  当属性发生变化的时候,就会调用metadata中传入的委托函数。这个过程是这样的, DependencyObject中定义一个虚函数 :

protected virtual void OnPropertyChanged(DependencyPropertyChangedEventArgs e)

  当DP发生变化的时候就会先首先调用到这个OnPropertyChanged函数,然后如果metaData中设置了PropertyChangedCallback的委托,再调用委托函数。这里我们设置了FrameworkPropertyMetadataOptions.AffectsMeasure, 意思是这个DP变化的时候需要重新测量控件和子控件的Size。具体WPF的实现就是FrameworkElement这个类重载了父类DependencyObject的OnPropertyChanged方法,在这个方法中判断参数中的metadata是否是FrameworkPropertyMetadata,是否设置了 FrameworkPropertyMetadataOptions.AffectsMeasure这个标志位,如果有的话调用一下自身的InvalidateMeasure函数。

  简要的谈了一下DependencyProperty,除了微软那种自卖自夸,这个DependencyProperty究竟为我们设计实现带来了哪些好处呢?   1. 就是DP本身带有的PropertyChangeCallback等等,方便我们的使用。   2. DP的动态性,也可以叫做灵活性。举个例子,传统的属性,需要在设计类的时候设计好,你在汽车里拿飞机翅膀肯定是不可以的。可是DependencyObject,通过GetValue和SetValue来模仿属性,相对于每个DependencyObject内部有一个百宝囊,你可以随时往里放置数据,需要的时候又可以取出来。当然,前面的例子都是使用一个传统的CLR属性来封装了DP,看起来和传统属性一样需要声明,下面介绍一下WPF中很强大的Attached Property。

  再谈Attached Property之前,我打算和朋友们谈一个设计模式,结合项目实际,会更有助于分析DP,这就是MVVM(Mode-View-ViewModel)。关于这个模式,网上也有很多论述,也是我经常使用的一个模式。那么,它有什么特点,又有什么优缺点呢?先来看一个模式应用:

public class NameObject : INotifyPropertyChanged
    {
        private string _name = "name1";
        public string Name
        {
            get
            {
                return _name;
            }
            set
            {
                _name = value;
                NotifyPropertyChanged("Name");
            }
        }

        private void NotifyPropertyChanged(string name)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(name));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }
    public class NameObjectViewModel : INotifyPropertyChanged
    {

        private readonly NameObject _model;

        public NameObjectViewModel(NameObject model)
        {
            _model = model;
            _model.PropertyChanged += new PropertyChangedEventHandler(_model_PropertyChanged);
        }

        void _model_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            NotifyPropertyChanged(e.PropertyName);
        }

        public ICommand ChangeNameCommand
        {
            get
            {
                return new RelayCommand(
                         new Action<object>((obj) =>
                        {

                             Name = "name2";

                        }),
                         new Predicate<object>((obj) =>
                        {
                             return true;
                        }));
            }
        }

        public string Name
        {
            get
            {
                return _model.Name;
            }
            set
            {
                _model.Name = value;
            }
        }

        private void NotifyPropertyChanged(string name)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(name));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
}
public class RelayCommand : ICommand
    {
        readonly Action<object> _execute;
        readonly Predicate<object> _canExecute;

        public RelayCommand(Action<object> execute, Predicate<object> canExecute)
        {
            _execute = execute;
            _canExecute = canExecute;
        }

        public bool CanExecute(object parameter)
        {
            return _canExecute == null ? true : _canExecute(parameter);
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public void Execute(object parameter)
        {
            _execute(parameter);
        }
    }
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
            this.DataContext = new NameObjectViewModel(new NameObject());
        }
    }
<Window x:Class="WpfApplication7.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window1" Height="300" Width="300">
    <Grid>
        <TextBlock Margin="29,45,129,0" Name="textBlock1" Height="21" VerticalAlignment="Top"
                   Text="{Binding Path=Name}"/>
        <Button Height="23" Margin="76,0,128,46" Name="button1" VerticalAlignment="Bottom"
                Command="{Binding Path=ChangeNameCommand}">Rename</Button>
    </Grid>
</Window>

类的关系如图所示:

c3d.club

  这里NameObject -> Model,NameObjectViewModel -> ViewModel,Window1 -> View。我们知道,在通常的Model-View世界中,无论MVC也好,MVP也好,包括我们现在提到的MVVM,它的Model和View的功能都类似,Model是用来封装核心数据,逻辑与功能计算的模型,View是视图,具体可以对应到窗体(控件)等。那么View的功能主要有,把Model的数据显示出来,响应用户的操作,修改Model,剩下Controller或Presenter的功能就是要组织Model和View之间的关系,整理一下Model-View世界中的需求点,大致有:   1. 为View提供数据,如何把Model中的数据提供给View。   2. Model中的数据发生变化后,View如何更新视图。   3. 根据不同的情况为Model选择不同的View。   4. 如何响应用户的操作,鼠标点击或者一些其他的事件,来修改Model。

  所谓时势造英雄,那么WPF为MVVM打造了一个什么“时势“呢。

  1. FrameworkElement类中定义了属性DataContext(数据上下文),所有继承于FrameworkElement的类都可以使用这个数据上下文,我们在XAML中的使用类似Text=”{Binding Path=Name}”的时候,隐藏的含义就是从这个控件的DataContext(即NameObjectViewModel)中取它的Name属性。相当于通过DataContext,使View和Model中存在了一种松耦合的关系。
  2. WPF强大的Binding(绑定)机制,可以在Model发生变化的时候自动更新UI,前提是Model要实现INotifyPropertyChanged接口,在Model数据发生变化的时候,发出ProperyChaned事件,View接收到这个事件后,会自动更新绑定的UI。当然,使用WPF的DenpendencyProperty,发生变化时,View也会更新,而且相对于使用INotifyPropertyChanged,更为高效。
  3. DataTemplate和DataTemplateSelector,即数据模板和数据模板选择器。可以根据Model的类型或者自定义选择逻辑来选择不同的View。
  4. 使用WPF内置的Command机制,相对来说,我们对事件更为熟悉。比如一个Button被点击,一个Click事件会被唤起,我们可以注册Button的Click事件以处理我们的逻辑。在这个例子里,我使用的是Command=”{Binding Path=ChangeNameCommand}”,这里的ChangeNameCommand就是DataContext(即NameObjectViewModel)中的属性,这个属性返回的类型是ICommand。在构建这个Command的时候,设置了CanExecute和Execute的逻辑,那么这个ICommand什么时候会调用,Button Click的时候会调用么?是的,WPF内置中提供了ICommandSource接口,实现了这个接口的控件就有了触发Command的可能,当然具体的触发逻辑要自己来控制。Button的基类ButtonBase就实现了这个接口,并且在它的虚函数OnClick中触发了这个Command,当然,这个Command已经被我们绑定到ChangeNameCommand上去了,所以Button被点击的时候我们构建ChangeNameCommand传入的委托得以被调用。

  正是借助了WPF强大的支持,MVVM自从提出,就获得了好评。那么总结一下,它真正的亮点在哪里呢?

  1. 使代码更加干净,我没使用简洁这个词,因为使用这个模式后,代码量无疑是增加了,但View和Model之间的逻辑更清晰了。MVVM致力打造一种纯净UI的效果,这里的纯净指后台的xaml.cs,如果你编写过WPF的代码,可能会出现过后台xaml.cs代码急剧膨胀的情况。尤其是主window的后台代码,动则上千行的代码,整个window内的控件事件代码以及逻辑代码混在一起,看的让人发恶。
  2. 可测试性。更新UI的时候,只要Model更改后发出了propertyChanged事件,绑定的UI就会更新;对于Command,只要我们点击了Button,Command就会调用,其实是借助了WPF内置的绑定和Command机制。如果在这层意思上来说,那么我们就可以直接编写测试代码,在ViewModel上测试。如果修改数据后得到了propertyChanged事件,且值已经更新,说明逻辑正确;手动去触发Command,模拟用户的操作,查看结果等等。就是把UnitTest也看成一个View,这样Model-ViewModel-View和Model-ViewModel-UnitTest就是等价的。
  3. 使用Attached Behavior解耦事件,对于前面的例子,Button的点击,我们已经尝试了使用Command而不是传统的Event来修改数据。是的,相对与注册事件并使用,无疑使用Command使我们的代码更“和谐“,如果可以把控件的全部事件都用Command来提供有多好,当然,控件的Command最多一个,Event却很多,MouseMove、MouseLeave等等,指望控件暴露出那么多Command来提供绑定不太现实。这里提供了一个Attached Behavior模式,目的很简单,就是要注册控件的Event,然后在Event触发时时候调用Command。类似的Sample如下:
        public static DependencyProperty PreviewMouseLeftButtonDownCommandProperty = DependencyProperty.RegisterAttached(
                "PreviewMouseLeftButtonDown",
                typeof(ICommand),
                typeof(AttachHelper),
                new FrameworkPropertyMetadata(null, new PropertyChangedCallback(AttachHelper.PreviewMouseLeftButtonDownChanged)));

        public static void SetPreviewMouseLeftButtonDown(DependencyObject target, ICommand value)
        {
            target.SetValue(AttachHelper.PreviewMouseLeftButtonDownCommandProperty, value);
        }

        public static ICommand GetPreviewMouseLeftButtonDown(DependencyObject target)
        {
            return (ICommand)target.GetValue(AttachHelper.PreviewMouseLeftButtonDownCommandProperty);
        }

        private static void PreviewMouseLeftButtonDownChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
        {
            FrameworkElement element = target as FrameworkElement;
            if (element != null)
            {
                if ((e.NewValue != null) && (e.OldValue == null))
                {
                    element.PreviewMouseLeftButtonDown += element_PreviewMouseLeftButtonDown;
                }
                else if ((e.NewValue == null) && (e.OldValue != null))
                {
                    element.PreviewMouseLeftButtonDown -= element_PreviewMouseLeftButtonDown;
                }
            }
        }

        private static void element_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            FrameworkElement element = (FrameworkElement)sender;
            ICommand command = (ICommand)element.GetValue(AttachHelper.PreviewMouseLeftButtonDownCommandProperty);
            command.Execute(sender);

  这里用到了DependencyProperty.RegisterAttached这个AttachedProperty,关于这个模式,留到下面去讲,这段代码的主要意思就是注册控件的PreviewMouseLeftButtonDown事件,在事件唤起时调用AttachedProperty传入的Command。

  那么是不是这个模式真的就这么完美呢,当然不是,MVVM配上WPF自然是如鱼得水,不过它也有很多不足,或者不适合使用的场合:

  1. 这个模式需要Model-ViewModel,在大量数据的时候为每个Model都生成这样一个ViewModel显然有些过。ViewModel之所以得名,因为它要把Model的属性逐一封装,来给View提供绑定。

  2. Command的使用,前面提到过,实现ICommandSource的接口才具备提供Command的能力,那是不是WPF的内置控件都实现了这样的接口呢?答案是不是,很少,只有像Button,MenuItem等少数控件实现了这一接口,像我们比较常用ComboBoxItem就没有实现这一接口。接口没实现,我们想使用Command的绑定更是无从谈起了。这个时候我们要使用Command,就不得不自己写一个ComboxBoxCommandItem继承于ComboBoxItem,然后自己实现ICommandSource,并且在Click的时候触发Command的执行了。看起来这个想法不算太好,那不是要自己写很多控件,目的就是为了用Command,也太为了模式而模式了。但像Expression Blend,它就是定义了很多控件,目的就是为了使用Command,说起来也奇怪,自己设计的控件,用起来自己还需要封装,这么多个版本也不添加,这个有点说不过去了。

  3. 纯UI,就是在控件后台的cs代码中除了构造函数最多只有一行,this.DataContext = xx; 设置一下数据上下文。当然,我目前的项目代码大都是这样的,还是那句话,不要为了模式而模式。那么多的控件event,不管是使用Attached模式还是用一些奇技淫巧用反射来构建出Command,都没什么必要。目前我的做法就是定义一个LoadedCommand,在这个Command中引用界面上的UI元素,ViewModel拿到这个UI元素后在ViewModel中注册控件事件并处理。还是第一个优点,这么做只是为了让代码更干净,逻辑更清晰,如果都把各个控件事件代码都写在一个xaml.cs中看起来比较混乱。

  谈过了MVVM,接下来重点谈AttachedProperty,这个是很好很强大的feature,也是WPF真正让我有不一样感觉的地方。前面简单谈过了DependencyProperty的原理,很多初接触WPF的朋友们都会觉得DP很绕,主要是被它的名字和我们的第一直觉所欺骗。如果我们定义了一个DP,MyNameProperty,类型是string的。那么在DependencyObject上,我谈过了有个百宝囊,就是EffectiveValueEntry数组,它内部最终储存MyName的值也是string,这个DependencyProperty(即MyNameProperty)是个静态属性,是在你设置读取这个string的时候起作用的,如何起作用是通过它注册时定义的propertyMetadata决定的。

  简单来说就是DependencyObject可以使用DependencyProperty,但两者没有从属关系,你在一个DependencyObject中定义了一个DP,在另一个DependencyObject也可以使用这个DP,你在另一个DependencyObject中写一个CLR属性使用GetValue和SetValue封装这个DP是一样的。唯一DependencyProperty和DependencyObject有关联的地方就是你注册的时候,DP保存在全局静态DP的Hashtable里的键值是通过注册时的名字和这个DependencyObject的类型的hashcode取异或生成的。但这个键值也可以不唯一,DP提供了一个AddOwner方法,你可以为这个DP在全局静态DP中提供一个新键值,当然,这两个键值指向同一个DP。

  既然如此,那么为什么有DependencyProperty.Register和DependencyProperty.RegisterAttached两种方法注册DP呢。既然DP只是一个引子,通过GetValue和SetValue,传入DependencyObject就可以取得存储在其中EffectiveValueEntry里面的值,这两个不是一样的么?恩,原理上是一个,区别在于,前面提到,一个DependencyProperty里面会有多个propertyMetadata,比如说Button定义了一个DP,我们又写了一个CustomButton,继承于Button。我们在CustomButton的静态函数中调用了前面DP的OverrideMetadata函数,DP的OverrideMetadata会涉及到Merge操作,它要把新旧的propertyMetadata合二为一成一个,作为新的propertyMetadata,而这个overrideMetadata过程需要调用时传入的类型必须是DependencyObject的。DependencyProperty.Register和DependencyProperty.RegisterAttached的区别是前者内部调用了OverrideMetadata而后者没有,也就意味着Rigister方法只能是DependencyObject调用,而后者可以在任何对象中注册。

  就这一个区别么?恩,还有的,默认的封装方法,Register是使用CLR属性来封装的,RegisterAttached是用静态的Get,Set来封装的。Designer反射的时候,遇到静态的封装会智能感知成类似Grid.Column=“2”这样的方式。这个就类似于非要说菜刀有两大功能,一是砍菜,二是砍人。你要感到纳闷,不是因为菜刀有刀刃么?它会和你解释,不同不同,砍菜进行了优化,你可以用手握着,砍人犯法,最好飞出去…

  那么为什么微软要把这个注册过程分为Register和RegisterAttached两类呢?就是为了强调Attach这个概念,这个过程就是DependencyObject(相当于银行金库,有很多箱子)通过DependencyProperty(相当于开箱子的钥匙)取得自己箱子里的财宝一样,当然这些所有的钥匙有人统一管理(全局的HashTable),你来拿钥匙的时候还要刁难一下你(通过钥匙上的附带的propertyMetadata)检查一下你的身份啦,你存取东西要发出一些通知啦等等。这个Attach,翻译过来叫附加,所谓的AttachedProperty(附加属性),就是说人家可以随时新配一把钥匙来你这新开一个箱子,或者拿一把旧钥匙来你这新开个箱子,谁让你箱子多呢?

  强调了这么多,只是为了说明一点,这个Attach的能力不是因为你注册了RegisterAttached才具备的,而是DependencyProperty本身设计就支持的。那么这个设计能为我们开发程序带来哪些好处呢?

  从继承和接口实现来说,人们初期阶段有些乱用继承,后来出现了接口,只有明确有IS-A语义的才用继承,能力方面的用接口来支持。比如飞行,那么一般会定义到一个IFlyable的接口,我们实现这个接口以获得飞行的能力。那么这个能力的获得要在类的设计阶段继承接口来获得,那么作为一个已经成熟的人,我是大雄,我要飞,怎么办?

AttachedProperty来救你。代码如下:

    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
            this.DataContext = new DragonFlyViewModel();
        }
    }
    public interface IFlyHandler
    {
        void Fly();
    }
    public class DragonFlyViewModel : IFlyHandler
    {
        public void Fly()
        {
            MessageBox.Show("送你个竹蜻蜓,飞吧!");
        }
    }
    public class FlyHelper
    {
        public static readonly DependencyProperty FlyHandlerProperty =
            DependencyProperty.RegisterAttached("FlyHandler", typeof(IFlyHandler), typeof(FlyHelper),
                new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnFlyHandlerPropertyChanged)));

        public static IFlyHandler GetFlyHandler(DependencyObject d)
        {
            return (IFlyHandler)d.GetValue(FlyHandlerProperty);
        }

        public static void SetFlyHandler(DependencyObject d, IFlyHandler value)
        {
            d.SetValue(FlyHandlerProperty, value);
        }

        public static void OnFlyHandlerPropertyChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
        {
            FrameworkElement element = target as FrameworkElement;
            if (element != null)
            {
                IFlyHandler flyHander = e.NewValue as IFlyHandler;
                element.MouseLeftButtonDown += new MouseButtonEventHandler((sender, ex) =>
                {
                    if (flyHander != null)
                    {
                        flyHander.Fly();
                    }
                });
            }
        }
    }
<Window x:Class="WpfApplication7.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication7"
        Title="Window1" Height="300" Width="300">
    <Grid>
        <Label Margin="72,58,88,113" Name="label1" Background="Yellow"
               local:FlyHelper.FlyHandler="{Binding}">我叫大雄我不会飞</Label>
    </Grid>
</Window

  这是一个最简单的模式应用,当然,还不是很完美,不过已经可以起飞了。我们在FlyHelper中使用DependencyProperty.RegisterAttached注册了一个AttachedProperty,在OnFlyHandlerPropertyChanged中订阅了element的MouseLeftButtonDown事件,事件处理就是”起飞”。这里定义了一个IFlyHandler的接口,使用ViewModel模式,ViewModel去实现这个接口,然后使用local:FlyHelper.FlyHandler=”{Binding}”绑定,这里{Binding}没有写path,默认绑定到DataContext本身,也就是DragonFlyViewModel上。

  你说什么?你要去追小静?那一定要帮你。你往脑门上点一下,看,是不是会飞了?怎么样,戴着竹蜻蜓的感觉很好吧,^_^。大雄欣喜若狂,连声感谢。不过,这么欺骗一个老实人的感觉不太好,实话实说了吧。你真是有宝物不会用啊,你胸前挂着那是啥?小口袋?百宝囊?那是机器猫的口袋,汽车大炮时空飞船,什么掏不出来啊。哦,你嫌竹蜻蜓太慢了?你等等。

    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
            this.DataContext = new DragonFlyViewModel();
        }

        private void button1_Click(object sender, RoutedEventArgs e)
        {
            this.DataContext = new FighterViewModel();
        }

    public interface IFlyHandler
    {
        void Fly();
    }

    public class DragonFlyViewModel : IFlyHandler
    {
        public void Fly()
        {
            MessageBox.Show("送你个竹蜻蜓,飞吧!");
        }
    }

    public class FighterViewModel : IFlyHandler
    {
        public void Fly()
        {
            MessageBox.Show("送你驾战斗机,为了小静,冲吧!");
        }
}
    public class FlyHelper
    {
        private IFlyHandler _flyHandler;

        public FlyHelper(IFlyHandler handler, FrameworkElement element)
        {
            _flyHandler = handler;
            element.MouseLeftButtonDown += new MouseButtonEventHandler(element_MouseLeftButtonDown);
        }

        void element_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (_flyHandler != null)
            {
                _flyHandler.Fly();
            }
        }

        private void UpdateFlyHandler(IFlyHandler handler)
        {
            _flyHandler = handler;
        }

        #region FlyHelper
        public static readonly DependencyProperty FlyHelperProperty =
            DependencyProperty.RegisterAttached("FlyHelper", typeof(FlyHelper), typeof(FlyHelper),
                new FrameworkPropertyMetadata(null));

        public static FlyHelper GetFlyHelper(DependencyObject d)
        {
            return (FlyHelper)d.GetValue(FlyHelperProperty);
        }

        public static void SetFlyHelper(DependencyObject d, FlyHelper value)
        {
            d.SetValue(FlyHelperProperty, value);
        }
        #endregion

        #region FlyHandler
        public static readonly DependencyProperty FlyHandlerProperty =
            DependencyProperty.RegisterAttached("FlyHandler", typeof(IFlyHandler), typeof(FlyHelper),
                new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnFlyHandlerPropertyChanged)));

        public static IFlyHandler GetFlyHandler(DependencyObject d)
        {
            return (IFlyHandler)d.GetValue(FlyHandlerProperty);
        }

        public static void SetFlyHandler(DependencyObject d, IFlyHandler value)
        {
            d.SetValue(FlyHandlerProperty, value);
        }

        public static void OnFlyHandlerPropertyChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
        {
            FrameworkElement element = target as FrameworkElement;
            if (element != null)
            {
                FlyHelper helper = (FlyHelper)element.GetValue(FlyHelperProperty);
                if (helper == null)
                {
                    IFlyHandler handler = e.NewValue as IFlyHandler;
                    if (handler != null)
                    {
                        helper = new FlyHelper(handler, element);
                        element.SetValue(FlyHelperProperty, helper);
                    }
                }
                else
                {
                    IFlyHandler handler2 = e.NewValue as IFlyHandler;
                    //handler2 may be null, this usually happened when this.DataContext = null, release IFlyHandler.
                    helper.UpdateFlyHandler(handler2);
                }
            }
        }
        #endregion       
}

  这里就是一个完整的Attached模式,这里我添加了一个新的AttachedProperty,类型是FlyHelper,当local:FlyHelper.FlyHandler=”{Binding}”绑定值发生变化时,判断传入的这个DependencyObject内是否有FlyHelper对象,没有,构造一个,然后塞入到这个DependencyObject中去;如果有,则更新FlyHelper内持有的IFlyHandler对象。这个Attached模式的好处在于,这个辅助的Helper对象是在运行时构造的,构造之后塞入到UI对象(DependencyObject)中去,仅是UI对象持有这个引用,UI对象被释放后这个Helper对象也被释放。FlyHelper对象用于控制何时”起飞”,至于怎么飞则依赖于IFlyHandler这个接口,这层依赖是在绑定时注入的,而这个绑定最终是运用了DataContext这个数据上下文,和MVVM模式搭配的很完美。这也就是MVVM模式中强调的,也就是唯一的依赖,设置控件的DataContext。

  回顾一下,作于例子中的Label,是不具备“飞行“能力的。这种不具备具体说就是不知道什么时候触发动作,也不知道触发了之后该干什么。通过一个Attach模式使它具备了这个能力,而且可以随时更新动作。简直达到了一种让你飞,你就飞的境界,值得为它喝彩。

  鉴于这种动态添加控件的能力,这种模式也被称为Attached Behavior。在Blend 3中,也加入了Behaviors的支持,很多通用的能力,都可以用Behavior来把它抽出来,比如缩放,DragDrop等等。我没有具体研究过Blend的Behavior,应该也是这种方法或演变吧。在实际项目中,我也大量使用了MVVM和Attached Behavior,配上CommandBinding,Unit Test,脚本化UIAutomation,以及Prism等框架,对一些比较大型的项目,还是很有帮助的。

  顺着DP这条线讲下来,还是蛮有味道的。当然,WPF中还有一些比较新的概念,包括逻辑树和视觉树,路由事件,Style和Template等等。其实纵看WPF,还是有几条主线的,包括刚才讲到的DP,Threading Model与Dispatcher,视觉树和依赖它产生的路由,Template和Style等等。那么就回到开头了,如何学好WPF呢?

  其实写这篇文章之前,我是经常带着这疑问的。现在新技术推出的很快,虽说没什么技术是凭空产生,都是逐渐衍变而来的。可是真学下去也要花成本,那怎么样才是学好了呢,怎么能融入到项目呢?后来总结了下,我需要了解这么一些情况:

  1. 这门技术是否成熟,前景如何?
  2. 摆脱宣传和炒作,这门技术的优缺点在哪里?
  3. 希望看到一些对这门技术有整体把握的文章,可以不讲细节,主要是帮我理出一个轮廓,最好和我的知识树连一连。
  4. 有没有应用的成功案例。
  5. 最重要的,呵呵,有没有可以下载的电子书。

  关于WPF,现在讲解的书籍和资料已经蛮多了。随着.NET Framework的升级,包括性能以及辅助工具的支持也越来越好了。但不得不说,WPF学习的时间成本还是很大的。WPF的设计很重,带着很浓的设计痕迹,查看WPF的源码,也许你会有种很熟悉的感觉。这种熟悉不是那种流畅美妙之感,到有种到了项目后期,拿着性能测试去优化,拿着Bug报告乱堵窟窿的感觉。

祝大家WPF愉快。…^_^