周银辉
1, 控件UI部分与逻辑部分的耦合.
这是一个容易被忽略但却非常重要的问题, 我们之所以使用CustomControl而不是UserControl,是因为我们希望自己的控件能向WPF内置控件一样,其UI能轻易地被其他用户定制或我们将来所改变.也就是说其视觉树不能与后台逻辑纠缠在一起,因为其视觉树中的元素完全可能被你的控件用户改变.比如,如果你的控件的视觉树中有一个Button,而你在该Button的Click事件中做了一些控件的逻辑处理,那么很可能你的控件打造失败了,因为该Button可能会在用户重新定义控件Template时被删除.
在讨论解决方案之前,需要提醒的是:一定要注意控件的逻辑与UI表现(Style,Template)各自职责的区分.不属于后台逻辑管的事情后台逻辑就不要管,不属于界面管的事情界面基本上也管不了或者说做起来很麻烦.一个简单的例子是:比如说你想鼠标移动到你的控件上的事情,控件稍稍变大一点,鼠标离开控件时控件大小又还原(或其他比较绚丽的效果),那么你在控件上的后台逻辑中添加的MouseEnter与MouseLeave事件的处理来达到这一效果.这时你的后台逻辑就管得过宽了,因为这种效果是Style的事情,你可以把它放在控件的默认Style中(在Generic.xaml中,你可以参考 在WPF中自定义控件(3) CustomControl (上) )来提供给控件用户而不应该加在后台逻辑中而费力不讨好.这不但增加了耦合,而且在用户看来这也有些"强奸民意",因为他没有办法通过自定义的Style来覆盖掉你认为比较漂亮的控件效果.
虽然WPF将UI与后台逻辑的隔离已经做得很不错了,以便UI设计师能和我们更好的沟通和分工协助,但这并不意味着,WPF可以将UI与后台完全的隔离而互不影响.事实上,我们在编写后台逻辑的时候常常需要用到控件UI树中的某些元素才能完成,比如在编写ProgressBar时我们需要知道视觉树中的某个表示"总量"的元素的长度或高度,以便根据ProgressBar的当前Value来确定视觉树中另外一个表示"当前量"的元素的长度或高度.还有一种情况是,我们后台写好了一个不错的逻辑,但需要视觉树中的某个UI元素来明确调用,比如说,我们在ScrollBar控件中写好了LineDown()方法,但该方法需要用户点击控件视觉树中某个表示"向下滚动一行"的元素(比如一个向下的箭头)时来调用.
WPF提供了两种方案,一是利用TemplatePartAttribute,二是使用Command.
1.1 TemplatePartAttribute
TemplatePart适用于上面所说的第一种情况,其用于告知用户,在目前的情况下必须在控件的视觉树中存在指定类型和名称的元素才能是控件发挥完整的功能,否则可能导致功能丧失或需要用户自行处理删除视觉树中的该元素而带来的后遗症.如果我们是某个控件的使用者,且其注明了该属性,那么我们在修改控件的Template时就应该保证控件中是指存在其指明的特定类型和名称的元素,除非了了解自己的确不需要其关联的相关功能或你已另有处理.
在WPF内置控件中,这种类型的控件很多,比如ComboBox,PasswordBox,ProgressBar等等.
我们看看ComboxBox:
![](/Images/OutliningIndicators/None.gif)
![](/Images/OutliningIndicators/None.gif)
![](/Images/OutliningIndicators/None.gif)
![](/Images/OutliningIndicators/None.gif)
![](/Images/OutliningIndicators/None.gif)
![](/Images/OutliningIndicators/None.gif)
我们的控件也可以仿照ComboBox来规定必须的部件,并Override一些 OnApplyTemplate()方法来取得相应元素:
![](/Images/OutliningIndicators/None.gif)
![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
![](/Images/OutliningIndicators/ExpandedBlockEnd.gif)
1.2 Command
这适合上面提到的第二种情况,即是我们后台写好了一个不错的逻辑,但需要视觉树中的某个UI元素来明确调用.比如ScrollBar的上端和下端的两个小箭头用来上下翻行,我们明显不能在这两个小箭头的鼠标点击事件中调用LineDown方法.那么正确的做法是,将后台逻辑中的LineDown和LineUp方法包装成LineDownCommand和LineUpCommand,然后将视觉树中的元素的Command属性绑定到相应的Command上.这样一来,即便用户修改视觉树中的上下小箭头为其他类型的元素,用户也可以通过命令绑定来与相应的功能联系起来.比如WPF内置的ScroolBar控件的向下小箭头的XAML代码便是如下书写的:
![](/Images/OutliningIndicators/None.gif)
2,"鹤立鸡群"并不总是好事
如果某天艺术细胞大爆发,打造了一个非常漂亮的控件,这自然是好事情,但我担心这与用户当前操作系统下的大多数界面显得过于鹤立鸡群而格格不入,毕竟在还是又不少人在Vista下使用"Windows经典"主题而非"Aero".为了打造与用户操作系统当前主题相容的控件UI,你可能需要为控件提供几套Style,比如一个比较相当较华丽的用于Aero主题,另一个较朴实用于Windows classical.为了实现效果随着用户操作系统主题改变而动态改变,你至少有两种方法来实现:(1)监听系统消息WM_THEMECHANGE,然后切控件界面.(2)将系统主题对应的Style放置在控件解决方案的themes文件夹下,比如与Vista Aero向对应的放在themes/Aero.NormalColor.xaml,与蓝色的Windows XP主题对应的放在themes/Luna.NormalColor.xaml,与Window经典主题相对应的放在themes/Classic.xmal,相信大家已经看出规律:themes/主题名.颜色名.xaml,其中经典主题没有颜色名.这样当用户切换主题时我们的控件就会切换到对应的Style,如果我们没有提供用户当前的主题所对应的样式则调用themes/Generic.xaml(这也就是为什么我在在WPF中自定义控件(3) CustomControl (上) 中说"Generic.xaml这个名称并非偶然"的原因)
3,关于控件资源的存储位置
一般说来,为了不破坏控件的封装性,我们不会把控件的资源放到控件以外的位置,比如,有一些资源在我们的应用中被频繁的使用,我们共享这些资源,我们可能会将这些资源移动到APP的资源字典中,但我们控件中的资源也被移出去,会破坏封装,并且这不利于控件被重用到其他APP. 但我们常常又会面临这样的问题:如果我把控件的资源完全放在该控件的资源字典中,但我们的应用很多地方使用了该控件,这就造成资源的频繁复制.一个典型的例子是,我们制作一个扑克牌游戏,我们的美工为我制作了一套漂亮的扑克牌图片,共54张图片,然后我建立一个扑克牌控件,控件实例将根据其当前点数和花色来选择其中一张图片并呈现出来,最后生成54个扑克牌控件实例来构成一套完整的扑克牌.如果我将美工提供的54张图片放置在控件的资源字典中,事实上对于一个扑克牌控件实例来说只使用了其中的一张图片,其余53张完全是多余的.而生成54张扑克牌控件实例时则相当于保存了54*54=2916张美工提供的图片.解决的办法是将资源转移到控件的themes/Generic.xaml中,这样既没有破坏封装又然资源得到了共享.
最后非常感谢大家关注我的博客,能在这里和大家分享工作与学习经验是件很美妙的事情.另外表示歉意的是:"在WPF中自定义控件"这个系列拖的时间太长了,差不多快一个半月了才算完成,因为这段时间我的确有太多的事情需要去完成.非常感谢大家.