周银辉
为快速地为你的应用定制一个零部件,你需要的是UserControl,这可以参考在WPF中自定义控件(2) UserControl, 为了让你打造的控件更标准化,更灵活以及更具有普遍意义,你需要用到的CustomControl,这正是本文要介绍的.
1,新建CustomControl
在选择控件基类后,第一件事情便是在你的项目中新建"CustomControl",我们会发现在项目中自动生成了一个*.CS(或*.VB或其他)文件以及\Themes\Generic.xaml(如果原来没有的话),他们分别是CustomControl的后台代码文件(Code Behind)与控件的默认主题文件,打开\Themes\Generic.xaml,你会发现其中自动生成了一个Style,这是你的控件的默认样式,正如WPF内置控件也有它的默认样式一样.这时,我们的工作就被分成了两个部分,一是在XXX.cs文件中编辑控件逻辑,而是在Generic.xaml中编写其UI.
2,Generic.xaml中的Style是如何与我们的控件联系在一起的
打开XXX.cs文件,你会发现静态构造方法中,VS自动地帮你覆盖了控件的DefaultStyleKey值:
static CustomControl1()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomControl1), new FrameworkPropertyMetadata(typeof(CustomControl1)));
}
我们知道DefaultStyleKeyProperty是FrameworkElement以及FrameworkContentElement类用来指示控件的默认样式键值的属性,该属性有一个很特别的地方就是我们不能够用继承的思想来思考它,比如说Button的默认样式键值是Style1,其子类MyButton的默认样式键值是Style2(或者没有指定默认样式),尽管MyButton可以向上转型成Button类,但我们并不希望其转型后的默认样式键值为Style1.所以WPF采用了在子类控件的静态构造方法中重写DefaultStyleKey元数据的方式来指定该子类控件的默认样式.上面代码中,我们将new FrameworkPropertyMetadata(typeof(CustomControl1))指定为其新的元数据值,这个值代表着,我们将在资源字典中查找一个键值为typeof(CustomControl1)的Style来做为控件的默认样式.而这个样式刚好被我们定义在了Generic.xaml中:{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomControl1), new FrameworkPropertyMetadata(typeof(CustomControl1)));
}
<Style TargetType="{x:Type local:CustomControl1}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:CustomControl1}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
这是大家可能有个疑问,上面XAML中的Style并没有指定Key值啊,而我们的控件要求的默认样式Key值为typeof(CustomControl1), 并且资源字典中的元素肯定是要有Key的? 这是Style的基本知识了,在WPF中,为Style指定Key时有两种方式:一是明确指定Key,而是在没有明确指定Key的情况下指定TargetType,WPF会自动地将其可Key设置为typeof(TargetType).如果你有在Blend中为控件打造Style的经验的话,你会注意到新建一个Style时,Blend会提供一个"Apply to ALL"选项,这也是为什么你打造的Style可以"Apply to all"的奥秘所在.<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:CustomControl1}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
3, "Generic.xaml"这个名称并非偶然
通过上面的叙述,你可能会有冲动将Generic.xaml中的Style代码剪切出来,粘贴到任何一个我们的控件可以找到的地方,然后把Generic.xaml删掉或改成更优雅的名称,如果你运气好的话,这是可行的,因为控件会自下而上(Page,App,Theme)去查找其所需要的Style,但此时你已经犯了一个潜在的错误:你没有为控件提供默认的样式.这里的默认样式其实是说"在默认主题中或没有为该控件找到当前操作系统对应的主题时采用的的样式".这涉及到WPF中Theme的相关话题了,有兴趣可以参考msdn相关SDK.
4,打造你的控件逻辑
这是必然的,添加属性,添加事件,方法等等,这些你可以参考在WPF中自定义控件(2) UserControl ,这里就不重复叙述了.
5,打造控件UI
这里值得一提的是我非常佩服在VS的XAML海洋里"裸泳"的兄弟们,不过我更推荐使用Microsoft Expression Blend来完成这项艰巨的任务.另外,如果你发现WPF内置控件在Blend中很好用而我们自己打造的控件却不是这样,那么请注意了,你的控件逻辑可能设计得不规范.
6,控件UI部分与逻辑部分的耦合度.
这是一个容易被忽略但却非常重要的问题, 我们之所以使用CustomControl而不是UserControl,是因为我们希望自己的控件能向WPF内置控件一样,其UI能轻易地被其他用户定制或我们将来所改变.也就是说其视觉树不能与后台逻辑纠缠在一起,因为其视觉树中的元素完全可能被你的控件用户改变.比如,如果你的控件的视觉树中有一个Button,而你在该Button的Click事件中做了一些控件的逻辑处理,那么很可能你的控件打造失败了,因为该Button可能会在用户重新定义控件Template时被删除.
关于如何将这种耦合降到最低,敬请关注"在WPF中自定义控件(3) CustomControl (下)"