[转]如何学好WPF
2021-03-11 11:27
标签:expr padding 之间 bsp 个数 learn 处理 元素 引擎 用了三年多的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来开发代码逻辑,这个想法稍有理想化: 在经历过这样不愉快的配合后,很多公司引入了一个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 1098 public types 1551 classes 777 public types 1500 classes 1577 public types 3592 classes 当然,这个数字未必准确,也不能由此说明WPF相比Asp.net、WinForm,有多复杂。但是面对如此庞大的类库,想要做到一览众山小也是很困难的。想要搞定一个大家伙,我们就要把握它的脉络,所谓庖丁解牛,也需要知道在哪下刀。在正式谈如何学好WPF之前,我想和朋友们谈一下如何学好一门新技术。 学习新技术有很多种途经,自学,培训等等。相对于我们来说,听说一门新技术,引起我们的兴趣,查询相关讲解的书籍(资料),边看书边动手写写Sample这种方式应该算最常见的。那么怎么样才算学好了,怎么样才算是学会了呢?在这里,解释下知识树的概念: 这不是什么创造性的概念,也不想就此谈大。我感觉学习主要是两方面的事情,一方面是向内,一方面是向外。这棵所谓树的底层就是一些基础,当然,只是个举例,具体图中是不是这样的逻辑就不要见怪了。学习,就是一个不断丰富自己知识树的过程,我们一方面在努力的学习新东西,为它添枝加叶;另一方面,也会不停的思考,理清脉络。这里谈一下向内的概念,并不是没有学会底层一些的东西,上面的东西就全是空中楼阁了。很少有一门技术是仅仅从一方面发展来的,就是说它肯定不是只有一个根的。比方说没有学过IL,我并不认为.NET就无法学好,你可以从另外一个根,从相对高一些的抽象上来理解它。但是对底层,对这种关键的根,学一学它还是有助于我们理解的。这里我的感觉是,向内的探索是无止境的,向外的扩展是无限可能的。 介绍了这个,接下来细谈一下如何学好一门新技术,也就是如何添砖加瓦。学习一门技术,就像新new了一个对象,你对它有了个大致了解,但它是游离在你的知识树之外的,你要做的很重要的一步就是把它连好。当然这层向内的连接不是一夕之功,可能会连错,可能会少连。我对学好的理解是要从外到内,再从内到外,就读书的例子谈一下这个过程: 市面关于技术的书很多,名字也五花八门的,简单的整理一下,分为三类,就叫V1,V2,V3吧。 回过头,就这个说一下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上的一个实践是这样的: 单看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, TypeownerType, PropertyMetadata typeMetadata, ValidateValueCallbackvalidateValueCallback); 其中第一和第三个参数就是用来确定HashTable中的键值,第二个参数确定了属性的类型,第四个参数是DP中的重点,定义了DP的属性元数据。在元数据中,定义了属性变化和强制值的Callback等。那么在一个SetValue的过程中,会出现哪些步骤呢: 当属性发生变化的时候,就会调用metadata中传入的委托函数。这个过程是这样的, DependencyObject中定义一个虚函数 : protected virtual voidOnPropertyChanged(DependencyPropertyChangedEventArgs e)。 当DP发生变化的时候就会先首先调用到这个OnPropertyChanged函数,然后如果metaData中设置了PropertyChangedCallback的委托,再调用委托函数。这里我们设置了FrameworkPropertyMetadataOptions.AffectsMeasure, 意思是这个DP变化的时候需要重新测量控件和子控件的Size。具体WPF的实现就是FrameworkElement这个类重载了父类DependencyObject的OnPropertyChanged方法,在这个方法中判断参数中的metadata是否是FrameworkPropertyMetadata,是否设置了 简要的谈了一下DependencyProperty,除了微软那种自卖自夸,这个DependencyProperty究竟为我们设计实现带来了哪些好处呢? 再谈Attached Property之前,我打算和朋友们谈一个设计模式,结合项目实际,会更有助于分析DP,这就是MVVM(Mode-View-ViewModel)。关于这个模式,网上也有很多论述,也是我经常使用的一个模式。那么,它有什么特点,又有什么优缺点呢?先来看一个模式应用: 类的关系如图所示: 这里NameObject -> Model,NameObjectViewModel-> ViewModel,Window1 -> View。我们知道,在通常的Model-View世界中,无论MVC也好,MVP也好,包括我们现在提到的MVVM,它的Model和View的功能都类似,Model是用来封装核心数据,逻辑与功能计算的模型,View是视图,具体可以对应到窗体(控件)等。那么View的功能主要有,把Model的数据显示出来,响应用户的操作,修改Model,剩下Controller或Presenter的功能就是要组织Model和View之间的关系,整理一下Model-View世界中的需求点,大致有: 所谓时势造英雄,那么WPF为MVVM打造了一个什么“时势“呢。 正是借助了WPF强大的支持,MVVM自从提出,就获得了好评。那么总结一下,它真正的亮点在哪里呢?
· 有些UI效果是很难或者不可以用XAML来描述的,需要手动编写效果。
· 大多数设计人员很难接受面向对象思维,包括对资源(Resource)的复用也不理想
· 用Blend生成的XAML代码并不高效,一种很简单的布局也可能被翻译成很冗长的XAML。
· V1类,名字一般比较好认,类似30天学通XXX,一步一步XXX…没错,入门类书。这种书大致上都是以展示为主的,一个一个Sample,一步一步的带你过一下整个技术。大多数我们学习也都是从这开始的,倒杯茶水,打开电子书,再打开VS,敲敲代码,只要注意力集中些,基本不会跟不上。学完这一步,你应该对这门技术有了一定的了解,当然,你脑海中肯定不自觉的为这个向内连了很多线,当然不一定正确,不过这个新东东的创建已经有轮廓了,我们说,已经达到了从外的目的。
· V2类,名字就比较乱了,其实意思差不多,只是用的词语不一样。这类有深入解析XXX,XXX本质论…这种书良莠不齐,有些明明是入门类书非要换个马甲。这类书主要是详细的讲一下书中的各个Feature, 来龙去脉,帮你更好的认识这门技术。如果你是带着问题去的,大多数也会帮你理清,书中也会顺带提一下这个技术的来源,帮你更好的把请脉络。这种书是可以看出作者的功力的,是不是真正达到了深入浅出。这个过程结束,我们说,已经达到了从外到内的目的。
· V3类,如果你认真,踏实的走过了前两个阶段,我觉得在简历上写个精通也不为过。这里提到V3,其实名字上和V2也差不多。往内走的越深,越有种冲动想把这东西搞透,就像被强行注入了内力,虽然和体内脉络已经和谐了,不过总该自己试试怎么流转吧。这里谈到的就是由内向外的过程,第一本给我留下深刻印象的书就是侯捷老师的深入浅出MFC,在这本书中,侯捷老师从零开始,一步一步的构建起了整个类MFC的框架结构。书读两遍,如醍醐灌顶,痛快淋漓。如果朋友们有这种有思想,讲思想,有匠心的书也希望多多推荐,共同进步。
public static readonly DependencyProperty IsSpinningProperty = DependencyProperty.Register("IsSpinning", typeof(bool));
public bool IsSpinning
{
get { return (bool)GetValue(IsSpinningProperty); }
set { SetValue(IsSpinningProperty, value); }
}
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));
FrameworkPropertyMetadataOptions.AffectsMeasure这个标志位,如果有的话调用一下自身的InvalidateMeasure函数。
1. 就是DP本身带有的PropertyChangeCallback等等,方便我们的使用。
2. DP的动态性,也可以叫做灵活性。举个例子,传统的属性,需要在设计类的时候设计好,你在汽车里拿飞机翅膀肯定是不可以的。可是DependencyObject,通过GetValue和SetValue来模仿属性,相对于每个DependencyObject内部有一个百宝囊,你可以随时往里放置数据,需要的时候又可以取出来。当然,前面的例子都是使用一个传统的CLR属性来封装了DP,看起来和传统属性一样需要声明,下面介绍一下WPF中很强大的Attached Property。
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 PropertyChangedEventHandlerPropertyChanged;
}
public class NameObjectViewModel : INotifyPropertyChanged
{
private readonly NameObject _model;
public NameObjectViewModel(NameObjectmodel)
{
_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 Actionobject>((obj) =>
{
Name = "name2";
}),
new Predicateobject>((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 PropertyChangedEventHandlerPropertyChanged;
}
public class RelayCommand : ICommand
{
readonly Actionobject> _execute;
readonly Predicateobject> _canExecute;
public RelayCommand(Actionobject> execute, Predicateobject> 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}">RenameButton>
Grid>
Window
1. 为View提供数据,如何把Model中的数据提供给View。
2. Model中的数据发生变化后,View如何更新视图。
3. 根据不同的情况为Model选择不同的View。
4. 如何响应用户的操作,鼠标点击或者一些其他的事件,来修改Model。
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传入的委托得以被调用。
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 DependencyPropertyPreviewMouseLeftButtonDownCommandProperty = DependencyProperty.RegisterAttached(
"PreviewMouseLeftButtonDown",
typeof(ICommand),
typeof(AttachHelper),
new FrameworkPropertyMetadata(null, new PropertyChangedCallback(AttachHelper.PreviewMouseLeftButtonDownChanged)));
pu