使用通用附加属性来减少WPF元素自定义样式的多余代码

本文将以WPFUI(https://gitee.com/dlgcy/WPFUI)项目中的 ComboBox样式为例,介绍如何使用附加属性来增强和简化样式代码。
一、自定义元素样式的方法
在开发 WPF 应用的过程中,我们常常需要给元素设置样式,其中一种方法是创建自定义样式,套路如下:
在设计器的元素上右键 --> 编辑模板 --> 编辑副本:.

使用通用附加属性来减少WPF元素自定义样式的多余代码
选择名称和位置后点击确定即可创建:

使用通用附加属性来减少WPF元素自定义样式的多余代码
创建后的样式如下,还包括一些颜色画刷之类的,还有最重要的 Template 属性中设置的控件模板及其触发器。在这基础上我们就可以大展拳脚,尽情改造了。

使用通用附加属性来减少WPF元素自定义样式的多余代码

二、使用样式继承减少重复代码
先来看看原始代码的情况:

使用通用附加属性来减少WPF元素自定义样式的多余代码

可以看到除了一些公用的代码外,主要给 ComboBox 提供了五个样式,五个样式之间就是颜色的差别,但是注意看前面的行号,每个样式还是都占用了大概 70 行,实际上其中很多代码是重复的,不相信的朋友可以亲自下载代码看看。
算了,还是我演示给大家看看吧,使用对比工具对比 PrimaryBox 和 SuccessBox 两个样式,可以看到除了三处颜色设置不同,其余代码都是重复的。三处颜色的不同,两处在普通属性设置区,一处在控件模板的触发器区,这个后面需要区别对待。

使用通用附加属性来减少WPF元素自定义样式的多余代码

对于普通属性区的重复,都不需要用到附加属性,直接一个继承就能解决了。可以再建一个基础样式,我这里直接把 PrimaryBox 当作基础样式,其余四个继承它即可。以 SuccessBox 为例,继承之后如下:

使用通用附加属性来减少WPF元素自定义样式的多余代码

可以看到,继承之后,普通属性设置区与基类样式相同的内容已经变灰了(Resharper 的功能),可以直接删除。由于模板属性(Template)中有一丁点的不同(前面说的那个颜色不同),导致整个模板设置都没有变灰,也就是暂时还不能删除。

三、通用附加属性代理类
接下来就是如何解决模板属性(Template)中的重复代码的问题了。
在继续之前,先来看看我之前为了让一个样式用于多个场景 —— 也就是让控件模板中的相关属性能在元素上进行设置 —— 是怎么做的吧。其实针对这种需求,有另一个做法:创建一个用户控件来继承这个元素,样式设置及最终使用都改为这个用户控件,然后需要新增设置的属性就在用户控件后台创建依赖属性。当时因为一是项目中不推荐为了这种情况创建用户控件,二是偷懒,三是对附加属性理解还不够没有想到用它,所以最终我是借用了元素(这里是 Button)自有的偏门的样式中暂未使用到的属性来传递需要的值的。比如为了设置圆角,我约定了使用 Button 的 TabIndex,然后控件模板中绑定给 Border 的 CornerRadius,并使用了 ObjectToIntConverter 转换器。还有其它几项也是这样:

使用通用附加属性来减少WPF元素自定义样式的多余代码

这个方案,怎么说呢,虽然能达到功能,但是缺点是显而易见的,而且不止一个:
1、方案非常规,使用别扭,如果不看样式上方的注释根本不知道怎么使用。
2、绑定不够直接,借用的属性类型往往与最终类型不同,需要加转换器。
3、占用原有属性,因为一旦被借用了,就不能用于原来的用途了,万一其它同事在使用的地方按照原意来使用这个被借用的属性,就会闹出笑话。
4、可被借用的属性数量有限,有可能满足不了需要个性化设置的地方数量。
5、等等......

后来某一天,我突然灵光乍现,想到可以创建一个通用的附加属性代理类(或者说是辅助类),来满足这种场景。其实如果去学习一些开源控件库,应该早就能发现这种用法了(后来在看AIStudio.Wpf.Controls的代码时验证了确实有这样用的),可惜没有如果,不过现在知道也不迟。
创建方法也很简单,随便建一个类(我这里是 WpfXamlPropProxy),让它继承 DependencyObject,然后在里面创建你需要的类型的附加属性即可。我这里建了圆角(CornerRadius)、边框粗细(BorderThickness)、鼠标移上的背景色(MouseOverBackground)三个附加属性,名称也是通用的:

使用通用附加属性来减少WPF元素自定义样式的多余代码

如果需要意义更明确,可以选择针对某个元素建立专用的代理类(比如 MahApps 的TextBoxHelper.Watermark这种的)
另外,附加属性的创建方法为,输入 propa 然后按两下 Tab 键插入代码片段:

使用通用附加属性来减少WPF元素自定义样式的多余代码

创建好了附加属性代理类,那么怎么使用呢?
首先,需要引入命名空间:
xmlns:attached="clr-namespace:WPFTemplateLib.Attached;assembly=WPFTemplateLib"

然后像前面那种借用元素自身属性的方案那样,只不过将那些属性替换为这个代理类中的属性即可,其实道理是一样的,附加属性也是依赖属性,只不过可以附加给别人罢了。这里有一个设置圆角的例子:

使用通用附加属性来减少WPF元素自定义样式的多余代码

这里样式中绑定了 WpfXamlPropProxy.CornerRadius,默认值为 5,在元素或者子样式中就可以对其更换为其它的值:

使用通用附加属性来减少WPF元素自定义样式的多余代码

四、使用附加属性让控件模板可共用
上一节介绍的使用通用的附加属性只是能够丰富可配置的内容,并没有减少样式代码,因为样式中的普通属性设置区,通过样式继承已经能够减少冗余了(见第二节),现在的关键是,如何去除样式中模板设置区的重复代码。答案还是使用附加属性,只不过不能直接使用,需要采用一种迂回的方法,接下来就介绍给大家,当然,如果大家有更好的方法,欢迎讨论。
在发现这个方法的过程中也走了些弯路,先来看看遇到的问题吧。

4.1、问题:给触发器中要设定的值绑定附加属性没效果
现象:在元素样式的控件模板的Triggers 中,在某个 Trigger 的某个 Setter 的 Value 中想绑定样式中设置的某个附加属性,结果提示找不到该属性:

使用通用附加属性来减少WPF元素自定义样式的多余代码

其它错误示范:如果在 Trigger(的 Setter)中直接使用 TemplateBinding,则直接会报错(不是有效值):

使用通用附加属性来减少WPF元素自定义样式的多余代码

网上的讨论:
关于 wpf:具有附加属性的模板绑定 | 码农家园 (codenong.com)
附加属性上的 WPF 触发器不起作用 - IT 工具网 (coder.work)

4.2、方法:使用代理元素在触发器中绑定附加属性
解决方法:在控件模板中添加一个隐藏的 “代理元素”,让它的某个合适的属性来绑定那个附加属性,然后在 Trigger 中再绑定这个代理元素的那个属性:

使用通用附加属性来减少WPF元素自定义样式的多余代码

本次这个 ComboBox 的也是同样的操作:

使用通用附加属性来减少WPF元素自定义样式的多余代码

示例代码地址:https://gitee.com/dlgcy/WPFTemplateLib/blob/master/Styles/DictionaryComboBox.xaml

五、效果展示
搞定了 Template 中的附加属性绑定问题后,子样式中的整个 Template 部分和主样式也就相同了,也就可以删除了。
所以最终的效果是很显著的,除了主样式的代码行数和之前差不多外,其余四个样式都只剩下区区几行了:

使用通用附加属性来减少WPF元素自定义样式的多余代码

效果如下:

使用通用附加属性来减少WPF元素自定义样式的多余代码

Demo 源码地址:https://gitee.com/dlgcy/DLGCY_WPFPractice/tree/Blog20221107

全文完,感谢阅读。