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

使用通用附加属性来减少 WPF 元素自定义样式的多余代码
魏刘宏 2022 年 11 月 07 日

本文将以WPFUI(https://gitee.com/dlgcy/WPFUI)项目中的 ComboBox样式为例,介绍如何使用附加属性来增强和简化样式代码。


一、自定义元素样式的方法
在开发 WPF 应用的过程中,我们常常需要给元素设置样式,其中一种方法是创建自定义样式,套路如下:
在设计器的元素上右键 --> 编辑模板 --> 编辑副本:

237948732bd4e02fc167e5ec91dbf79c.png


选择名称和位置后点击确定即可创建:

68bd0f814e6917673f2ebe5ca9cc5426.png



创建后的样式如下,还包括一些颜色画刷之类的,还有最重要的 Template 属性中设置的控件模板及其触发器。在这基础上我们就可以大展拳脚,尽情改造了。

541d84aa1d21e06a5cc1e1459b8587ea.png

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

2c91b2b14d6c8f5dc83416203dc73b16.png


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

18479007f565646e7e2bec4ad5bbc5fb.png

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

19311881100eb5e65dffd379bd69da58.png

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

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

cd1c130947c98c9e6ab9eec1d9dec61c.png



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

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

6454534aa83b1a68637c9c5040621a14.png

如果需要意义更明确,可以选择针对某个元素建立专用的代理类(比如 MahApps 的TextBoxHelper.Watermark这种的)

另外,附加属性的创建方法为,输入 propa 然后按两下 Tab 键插入代码片段:

8a54024778bed29905b3f30d2ff24779.png

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

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

4bd6aaad2f90cfad9695b399594c219e.png

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

b74a1cc58b4b5b929b5ea0c91bb0fdfe.png

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

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

5c4a545ec4ae35f3bf541d7ba80f662a.png

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

125170177d4e422a64ddc05a07b7fe3b.png

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

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

4df543632968a2f60abd3b8f56874e45.png

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

988e6a96a470c7b4d87c562b2d76f067.png

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

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

4835047728e888f50d11cebd2f54befc.png

效果如下:

c9829f44661df3f808166f85832f795a.png

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

全文完,感谢阅读。
技术群:添加小编微信并备注进群
小编微信:mm1552923   公众号:dotNet编程大全
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值