Windows Phone 7的发布日趋逼近,据说SDK开发包都已经出到beta了,虽然很久很久以前就一直有心开搞WPF,无奈惰性使然迟迟没有动作,如今倘若再不开搞,定会被如火如荼的移动时代淘汰了。再加之上次在深圳.NET俱乐部的活动中看了任旻大虾演示的QQ概念版,深为触动。本着从高从全的指导思想,打算从WPF入手然后再扩展到Silverlight和WP7。
在学习过程中我打算把一些所思所得记录下来,算是整理自己的思路同时也给一个爱好围观者的把戏。因为这只是个人的学习记录,其中有些代码未经调试,有些想法也是我连猜带蒙未经证实的胡思乱想,所以您且看罢了,如果真不幸给您造成了困扰、徒增了纠结,那绝对与微微软和WPF无关,也与本人无关,特此声明。
某个黄道吉日,俺从书柜中翻出已尘封两年左右的《WPF揭秘》(http://www.china-pub.com/39340),断续通过一个多礼拜的睡前翻读,感觉该书翻译还过得去,虽然间或有些话读起来略感别扭,但总体上还是畅顺的。期间照着书上的范例代码做了一个界面,至少已不像刚开始直接把玩WPF时有那么多的挫败感了,想当初在WPF中做个简单的界面都找不着北,积累那么多年WinForm和WebForm的经验在此完全失效,真是令人相当沮丧,因为WPF的设计完全颠覆了WinForm和WebForm中的设计理念,所以先系统的阅读WPF书籍对学习该项技术有莫大的帮助。
属性值的类型问题
因为WPF为了支持XAML这种XML语言的申明式语法,使用了大量的属性,这样这些属性就可以灰常方便的在XAML中以Attribute或者Element的方式进行申明定义,而在XAML中对各种对象的属性(Property)进行赋值首先面临的一个问题是类型转换,因为XAML这种XML语言是基于纯文本格式,而这些对象属性的类型却五花八门,既有原生的.Net类型也有自定义的各种对象类型,如何将XML中的Attribute和Element文本值转化成这些目标类型呢?正好System.ComponentModel.TypeConverter 这位仁兄就是为解决这个问题而生的,其中的两个核心方法:ConvertFromString 和 ConvertTo 方法分别负责将字符串转换成目标类型和将目标类型转换成一个字符串,其实在.NET中我们有意无意都接触到过这个类,只不过通常情况下它总是在后台默默工作不为人知罢了,譬如在WinForm的设计界面中,你可以直接在某个控件的属性框中的Size属性值的输入区直接敲入『200, 100』这样一个文本,那么后台设计器其实就是调用System.Drawing.SizeConverter类将这样一个逗号分隔的文本转换成System.Drawing.Size结构的,那么为什么设计器知道使用SizeConverter类来转换呢?这是因为 Size 结构使用TypeConverterAttribute声明了它的类型转换器就为SizeConverter了,这样设计器内部就可以使用System.ComponentModel.TypeDescriptor类的GetConverter方法来获得目标类型的真实的TypeConverter类了,如果你在.NET的各种设计器中看到需要将文本转换成一些目标类型的地方,后台基本都是TypeConverter在默默支持着。当然,如果目标对象过于复杂,要将它们完全无损的转换成文本,这无论是从可读性还是可行性上讲,都是不足取的,那么对象序列化技术就孕育而生了,说到这,好像扯远了,就此打住吧。
至此TypeConverter基本上已经能满足我们很多的应用场景了。诶,等等,如果某个对象的属性是对另外一个静态类的某个静态属性或字段的引用,这该如何使用文本来描述呢?又或者它是对运行时对象的引用,譬如是System.Type类型,这该如何是好呢?再者他引用的目标对象需要进行带参构造函数的构建,又该如何定义?这个,这个确实有点麻烦,所以WPF只好动用一个新武器了,这便是传说中的标记扩展(MarkupExtension),他使用了一种稍微特别的语法来定义这种东西,譬如,如果我们我们希望设置某属性值为空(null),可以如此撰写XAML:<Button Background=”{NullExtension}” /> (特注:该写法被删减过),您看到的那对花括号中的NullExtension就是一个标记扩展类,就像.NET框架中各种直接或间接继承自的System.Attribute的各种Attribute类,我们在使用这些类的时候可以省略后面的“Attribute”单词一样,在XAML中,应用各种标记扩展类时也可以省略后面的“Extension”单词,所以如上写法可以简写成<Button Background=”{Null}” />,呵呵,这下看起来总算舒服多了。再看一例:<Button Height=”{Static Member=SystemParameters.IconHeight}” />这在告诉StaticExtension类去获取SystemParameters静态类的IconHeight属性值,并将该静态属性值赋值给Button的Height属性。还有一种对标记扩展类带参构造函数调用的写法,俺就不啰嗦了,MSDN上有详尽说明。看到这个地方,俺想起了俺那心爱的插件框架,里面的那个解析器(Parser)就是专干这事的,只是写法略有不同罢了,唉,真是世事多巧合啊。
属性元素
这个东西比较简单,之所以把它记录在案,是因为很久很久以前,第一次翻看《WPF揭秘》一书的时候,我老记不住这个名词,要不就给记成“元素属性”了,现在之所以记得住,是因为知道了它的诞生缘由。首先我想这个东西的英文原词应该是“Property Element”吧(注:这是我瞎猜的,未经考证)。那么为啥要整这么一个概念出来咧?OK,我们来看一个例子便知:<Button Content=”I’m a button” />,这个XAML片段定义了一个按钮,按钮的内容是一个简单的文本,就跟我们在老式UI技术中的一样,平淡无奇。好了,无法忍受平淡的WPF要发威了——“翠花,上按钮”
<Button Padding="4">
<Button.Content>
<Grid>
<Ellipse Width="140" Height="40" Stroke="Red" Fill="Yellow" />
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center">I'm a evil button</TextBlock>
</Grid>
</Button.Content>
</Button>
好吧,我承认有一点按钮强迫症。这个邪恶的按钮里面被塞入了一个红色边框黄色背景的圆圈,中间还有一个文本,如果你比我更疯狂,还可以在里面继续放颗槟榔什么的。里面那个<Button.Content>此时就是属性元素的写法了,为啥叫属性元素呢,因为它表示Button的Content属性(Property),而这个属性是对应到XAML中的一个Element(元素)节点的,故而名为此。那么为啥要用元素(Element)的写法呢?那是因为XML的Attribute写法已经不堪重负,就算用TypeConverter也搞不定了这么复杂的描述了。再次强行插入一则广告:这跟俺在插件框架中的Builtin与子Builtin如出一辙,乖乖,莫非真存在心有灵犀这样的传说?
附加属性的XAML写法
当初为了在WPF中试验一个简单的界面,我需要使用到Docking布局,好歹是只玩弄WinForm多年的老菜鸟了,所以直接去某个简单控件中找Dock属性,乖乖一顿好找居然没找到,最终我在某个犄角旮旯发现了一个叫DockPanel的家伙,把它拖出来,然后再把俺的习惯性按钮添加进去,神奇的事情发生鸟,在这个按钮中突然崩出来一个叫DockPanel.Dock的属性来,至此佳节之际我想起一个久违的术语:扩展控件,在WinForm中,按钮是没有ToolTip这样的属性的,但是如果在窗体中放置一个ToolTip组件的话,那么该窗体中所有的控件都会出现一个神奇的“ToolTip上的ToolTip”属性,而ToolTip控件就叫扩展控件,所谓扩展控件就是实现了IExtenderProvider接口的组件或控件类。在WPF中的DockPanel与子控件的Dock交互跟WinForm里面的扩展控件的用法是不是很像呢?不过,经过后面阅读,我知道他们的内部机制可就完全没有可比性了,不要怪我老是拿WinForm来比照,你明白的,人的思维惯性是灰常强大的,不是变态一般是把控不了的。
赶紧回到刚才那个WPF中的按钮上来,现在我终于可以在设计视图中的属性框内设置按钮的Dock值了,设置成功后,在XAML中看起来就是这个样子:
<DockPanel Width="400" Height="400">
<Button DockPanel.Dock="Top" Content="1.Top" Height="40" />
<Button DockPanel.Dock="Left" Content="2.Left" Width="40" />
</DockPanel>
如上XAML片段用C#代码来写,大致如下(略有删减):
DockPanel panel = new DockPanel();
Button button1 = new Button()
{
Content = "1.Top",
Height = 40,
};
Button button2 = new Button()
{
Content = "2.Left",
Width = 40,
};
DockPanel.SetDock(button1, System.Windows.Controls.Dock.Top);
DockPanel.SetDock(button2, System.Windows.Controls.Dock.Left);
依赖属性
开发WPF程序,首推使用XAML,而在XAML中嵌入代码是件非常糟糕的事情,对此你可以回想下在ASP那个水深火热的旧社会中广大Coder们的悲惨生活。现在XAML的这个新时代,各种属性把劳苦的我们解放出来,但是传统的属性不具备灵活的通知机制,所以WPF推出了伟大的依赖属性和附加属性这么个核武器,事实上附加属性只是依赖属性的另一种形式的包装。它到底有多强悍,在此不表,因各种资料中都有详尽掐媚炫夸之辞。总之,在WPF的各种技术特性中它无时无刻无处不在,因此如果对依赖属性没有细致深入的理解,越学习到后面越是感觉如处迷雾中,但见远处的光亮却无从走近,所以我决定先停下来,把这个东西彻底搞清楚再继续前行。
因为编译器的各种处理,使得Reflector反编译出来源码不便阅读,因此我上微微软网站(请猛击此处:http://referencesource.microsoft.com/netframework.aspx)下载了.NET 4.0的源码。可惜,微微软提供的源码是供调试之用,因此没有以项目的形式进行包装组织,这点让人很是纠结,不知哪位兄弟有完整的像类似于SharpDevelop源码包这样的下载地址,还请不吝赐予。
OK,先上DependencyProperty类的Register方法:
1: public static DependencyProperty Register(string name, Type propertyType, Type ownerType,
PropertyMetadata typeMetadata, ValidateValueCallback validateValueCallback)
2: {
3: RegisterParameterValidation(name, propertyType, ownerType);
4:
5: //Register an attached property
6: PropertyMetadata defaultMetadata = null;
7: if(typeMetadata != null && typeMetadata.DefaultValueWasSet())
8: {
9: defaultMetadata = new PropertyMetadata(typeMetadata.DefaultValue);
10: }
11:
12: DependencyProperty property = RegisterCommon(name, propertyType, ownerType, defaultMetadata, validateValueCallback);
13:
14: if(typeMetadata != null)
15: {
16: //Apply type-specific metadata to owner type only
17: property.OverrideMetadata(ownerType, typeMetadata);
18: }
19:
20: return property;
21: }
该注册方法还有另外两个分别忽略最后两个参数的重载,即后两个参数可以为空(null)。该方法内部调用的第一个方法为RegisterParameterValidation,这只是一个检测前三个参数是否有效的验证方法,如果其中任何一个为空则抛出异常而已。第7行判断第三个类型为PropertyMetadata的参数是否不为空并且已经设置了默认值,如果是,则创建一个另外一个名为defaultMetadata的属性元数据对象,这里为啥要另外创建而不直接使用参数引用的那个PropertyMetadata对象呢?让我们先跟进去看看PropertyMetadata类的构造函数吧,这个构造函数只是简单将输入参数直接赋值给其DefaultValue属性而已,该属性代码如下:
1: public object DefaultValue
2: {
3: get
4: {
5: DefaultValueFactory defaultFactory = _defaultValue as DefaultValueFactory;
6: if(defaultFactory == null)
7: {
8: return _defaultValue;
9: }
10: else
11: {
12: return defaultFactory.DefaultValue;
13: }
14: }
15: set
16: {
17: if(Sealed)
18: {
19: throw new InvalidOperationException(SR.Get(SRID.TypeMetadataCannotChangeAfterUse));
20: }
21:
22: if(value == DependencyProperty.UnsetValue)
23: {
24: throw new ArgumentException(SR.Get(SRID.DefaultValueMayNotBeUnset));
25: }
26:
27: _defaultValue = value;
28:
29: SetModified(MetadataFlags.DefaultValueModifiedID);
30: }
31: }
在属性里面使用了DefaultValueFactory类,这是一个内部的抽象类,里面有两个抽象成员:CreateDefaultValue方法和DefaultValue只读属性,为啥要动用这么个东西来处理简单的默认值呢?我估计是因为在不同的依赖对象中可能需要对不同的依赖属性使用不同策略的默认值创建方式,那么可以定义不同创建策略的默认值实体工厂类来处理吧。在getter中,如果属性变量保存的默认值为DefaultValueFactory类型,则使用该工厂提供的默认值,否则直接返回对应的成员变量。在setter中,首先判断Sealed属性,顾名思义Sealed表示该PropertyMetadata修饰的依赖属性是否已经被封闭,若为真,则不允许更改其默认值(即抛出异常)。那么大致来看看该Sealed属性实现及其相关的其他代码吧:
1: // PropertyMetadata, UIPropertyMetadata, and FrameworkPropertyMetadata.
2: [FriendAccessAllowed] // Built into Base, also used by Core and Framework.
3: internal MetadataFlags _flags;
4:
5: private void SetModified(MetadataFlags id) { _flags |= id; }
6: private bool IsModified(MetadataFlags id) { return (id & _flags) != 0; }
7:
8: /// <summary>
9: /// Write a flag value
10: /// </summary>
11: [FriendAccessAllowed] // Built into Base, also used by Core and Framework.
12: internal void WriteFlag(MetadataFlags id, bool value)
13: {
14: if (value)
15: {
16: _flags |= id;
17: }
18: else
19: {
20: _flags &= (~id);
21: }
22: }
23:
24: /// <summary>
25: /// Read a flag value
26: /// </summary>
27: [FriendAccessAllowed] // Built into Base, also used by Core and Framework.
28: internal bool ReadFlag(MetadataFlags id) { return (id & _flags) != 0; }
29:
30: internal bool Sealed
31: {
32: [FriendAccessAllowed] // Built into Base, also used by Core.
33: get { return ReadFlag(MetadataFlags.SealedID); }
34: set { WriteFlag(MetadataFlags.SealedID, value); }
35: }
1: [FriendAccessAllowed] // Built into Base, also used by Core and Framework.
2: internal enum MetadataFlags : uint
3: {
4: DefaultValueModifiedID = 0x00000001,
5: SealedID = 0x00000002,
6: // Unused = 0x00000004,
7: // Unused = 0x00000008,
8: Inherited = 0x00000010,
9:
10: UI_IsAnimationProhibitedID = 0x00000020, // True if peer refers to an owner's animation peer property; False if Peer refers to the animation peer's owner property
11:
12: FW_AffectsMeasureID = 0x00000040,
13: FW_AffectsArrangeID = 0x00000080,
14: FW_AffectsParentMeasureID = 0x00000100,
15: FW_AffectsParentArrangeID = 0x00000200,
16: FW_AffectsRenderID = 0x00000400,
17: FW_OverridesInheritanceBehaviorID = 0x00000800,
18: FW_IsNotDataBindableID = 0x00001000,
19: FW_BindsTwoWayByDefaultID = 0x00002000,
20: FW_ShouldBeJournaledID = 0x00004000,
21: FW_SubPropertiesDoNotAffectRenderID = 0x00008000,
22: FW_SubPropertiesDoNotAffectRenderModifiedID = 0x00010000,
23: // Unused = 0x00020000,
24: // Unused = 0x00040000,
25: // Unused = 0x00080000,
26: FW_InheritsModifiedID = 0x00100000,
27: FW_OverridesInheritanceBehaviorModifiedID = 0x00200000,
28: // Unused = 0x00400000,
29: // Unused = 0x00800000,
30: FW_ShouldBeJournaledModifiedID = 0x01000000,
31: FW_UpdatesSourceOnLostFocusByDefaultID = 0x02000000,
32: FW_DefaultUpdateSourceTriggerModifiedID = 0x04000000,
33: FW_ReadOnlyID = 0x08000000,
34: // Unused = 0x10000000,
35: // Unused = 0x20000000,
36: FW_DefaultUpdateSourceTriggerEnumBit1 = 0x40000000, // Must match constants used in FrameworkPropertyMetadata
37: FW_DefaultUpdateSourceTriggerEnumBit2 = 0x80000000, // Must match constants used in FrameworkPropertyMetadata
38: }
Sealed属性就是调用ReadFlag和WriteFlag这两个内部方法进行状态读写操作,那么这个_flags变量和MetadataFlags枚举是啥意思呢?这其实是典型的使用二进制掩码的方式进行标记/状态判断的一种编码风格,这种方式在C/C++语言中使用的比较广泛,主要的好处就是比较节约内存,尤其是在较多状态开关的类中能以一种相对统一的方式进行设置或取值。对二进制和十六进制略有了解的童鞋应该都能看明白,我就不啰嗦了。
同时在setter中有个判断“if(value == DependencyProperty.UnsetValue)”,这个DependencyProperty.UssetValue是个啥?
public static readonly object UnsetValue = new NamedObject("DependencyProperty.UnsetValue");
UnsetValue字段是单例模式的命名对象,语意上表示一个未设置的值。因为任何时刻我们可以查看任何一个依赖属性的值是否等于DependencyProperty.UnsetValue单例对象来判断其属性值是否被设置过,故此,也必须确保默认值不能为该单例对象。因为NamedObject类只是个标记性的描述类,所以可以想见其代码非常简单,这里就不再贴出来了。
好了,让我们再回到DependencyProperty类的Register方法中来,其12行调用了RegisterCommon方法,这是个私有方法,也是真正干活的地方,具体怎么个干法呢,且待俺继续琢磨琢磨……