[WPF 自定义控件]开始一个自定义控件库项目
1. 目标
我实现了一个自定义控件库,并且打算用这个控件库作例子写一些博客。这个控件库主要目标是用于教学,希望通过这些博客初学者可以学会为自己或公司创建自定义控件,并且对WPF有更深入的了解。
image现阶段我的目标是实现一些简单的控件,由于我并不是打算重复造轮子,所以我会挑些Extended Wpf Toolkit没有的功能实现,之后再根据常用的UI模式慢慢增加各类控件和工具。(我一直在用Extended Wpf Toolkit,作为免费开源的控件库十分好用。)
因为自己很少通过VisualStudio的Toolbox添加控件,所以暂时不考虑添加工具箱支持,如有需要可以参考这篇文章。
要创建一个自定义控件库只需要在VisualStudio中新建项目并选择“WPF 自定义控件库”,但创建一个项目还有很多琐碎的需要考虑的地方,这篇文章主要介绍创建一个控件库项目需要考虑的内容。
2. 命名
万事起头难,最难的就是命名,控件库的命名也烦恼了我很久。
2.1 品牌名
如果是公司的项目,直接用公司名+产品名的组合就可以,但个人的项目就要另外考虑品牌名了。
品牌名有很多地方要考虑,例如不能使用带有贬义的名称。有涉及外观印象的词也要慎用,如Aqua,给人印象就是水的、蓝色的,如果以后要为控件库设计红色的主题就会很尴尬。诺基亚当年选择Lumia作为品牌连发音都有考虑到:
“在1980年全球只有10,000左右的注册科技商标,而如今光在美国,就有超过30万这样的注册商标。”克里斯说道,为此候选名单也从最初的200个一下锐减到为数不多的几个幸存者身上。
精通各地方言(84种语言)的语言学家们围绕这些为数不多的几个幸存者们开始工作,剔除其中某些会产生歧义的单词,并排除带有在某些国家很难发音的字母如J,LR和V,和在某些语言中不存在的字母(如在波兰语中没有的Q)的单词,以确保全球绝大多数国家和地区人民都能流畅的说出这一名称。
虽然只是个控件库而已不需要考虑这么多,但容易发音还是很重要的,最后我选了“kino”,没什么意义,只是简短好读而已。
2.2 程序集名称
上面提到的Extended Wpf Toolkit,程序集的名称是Xceed.Wpf.Toolkit;而WindowsCommunityToolkit的程序集名称是Microsoft.Toolkit。对这些著名控件库来说名称和程序集的名称不一致带来的影响应该不大,但我还是倾向控件库的名称和程序集的名称一致比较好,毕竟知名度不高的情况下,或者公司内部项目多的情况下很容易产生混乱。
《.NET设计规范:约定、惯用法与模式》这本书里提到:
- 要用公司名称作为名字控件的前缀,这样可以避免与另一家公司使用相同的名字。
- 要用稳定的、与版本无关的产品名称作为名字空间的第二层。
那么参考Extended Wpf Toolkit的习惯,程序集的名称应该就是Kino.Wpf.Toolkit。考虑到如果以后可能还需要实现别的类库,如Kino.Uwp.Toolkit,而这两个控件库共同引用一个基础类库的话,那这个基础类库不管是叫Kino.Wpf还是Kino.Uwp都比较尴尬。所以最后还是决定Kino.Tookit.Wpf这样的顺序。
复杂的控件,如DataGrid可以单独一个程序集(参考Microsoft.Toolkit.Uwp.UI.Controls.DataGrid),但我没打算做到这么复杂,目前一个程序集就够了。
3. 目录结构
我习惯为每一个(或每一组)控件单独建立一个目录,并且将各个控件的资源文件分开存放,再在Generic.xaml中合并它们。具体可以参考WindowsCommunityToolkit的做法:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ms-appx:///Microsoft.Toolkit.Uwp.UI.Controls/HamburgerMenu/HamburgerMenu.xaml" />
<ResourceDictionary Source="ms-appx:///Microsoft.Toolkit.Uwp.UI.Controls/HeaderedContentControl/HeaderedContentControl.xaml" />
<ResourceDictionary Source="ms-appx:///Microsoft.Toolkit.Uwp.UI.Controls/HeaderedItemsControl/HeaderedItemsControl.xaml" />
<ResourceDictionary Source="ms-appx:///Microsoft.Toolkit.Uwp.UI.Controls/RangeSelector/RangeSelector.xaml" />
<ResourceDictionary Source="ms-appx:///Microsoft.Toolkit.Uwp.UI.Controls/SlidableListItem/SlidableListItem.xaml" />
<ResourceDictionary Source="ms-appx:///Microsoft.Toolkit.Uwp.UI.Controls/ImageEx/ImageEx.xaml" />
<ResourceDictionary Source="ms-appx:///Microsoft.Toolkit.Uwp.UI.Controls/ImageEx/RoundImageEx.xaml" />
<ResourceDictionary Source="ms-appx:///Microsoft.Toolkit.Uwp.UI.Controls/HeaderedTextBlock/HeaderedTextBlock.xaml" />
<ResourceDictionary Source="ms-appx:///Microsoft.Toolkit.Uwp.UI.Controls/InfiniteCanvas/InfiniteCanvas.xaml" />
…
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
其它:
- Common目录,工具类放在这个目录;
- Converters目录,实现IValueConverter接口的类都放在这个目录;
- Assets及Assets/Images,存放图片等资源;
4. 命名空间
由于不打算把自定义控件库做得太复杂,目前所有控件都只使用Kino.Toolkit.Wpf这个命名空间。将来如果有一些高级特性或实验性质的控件,可以按照Wpf的惯例放在Kino.Toolkit.Wpf.Primitives里面。
更进一步的,可以添加如下代码指定XAML中的命名空间:
[assembly: XmlnsPrefix("https://github.com/DinoChan/Kino.Toolkit.Wpf", "kino")]
[assembly: XmlnsDefinition("https://github.com/DinoChan/Kino.Toolkit.Wpf", "Kino.Toolkit.Wpf")]
[assembly: XmlnsDefinition("https://github.com/DinoChan/Kino.Toolkit.Wpf", "Kino.Toolkit.Wpf.Primitives")]
然后在XAML中可以这样引用:
xmlns:kino="https://github.com/DinoChan/Kino.Toolkit.Wpf"
这样做的好处是可以忽略真实的命名空间,便于以后修改命名空间或API升级。
5. 版本号
程序集的版本号格式如下:
<主版本>.<次版本>.<生成号>.<修订版本>
不过平时我都没用到“修订版本”,只使用前三个。
Kino.Toolkit.Wpf则大致遵循语义化版本控制:
SemVer 的最基本方法是 3 组件格式 MAJOR.MINOR.PATCH
,其中:
- 进行不兼容的 API 更改时,
MAJOR
将会增加 - 以后向兼容方式添加功能时,
MINOR
将会增加 - 进行后向兼容 bug 修复时,
PATCH
将会增加
存在多处更改时,单个更改影响的最高级别元素会递增,并将随后的元素重置为零。 例如,当 MAJOR
递增时,MINOR
和 PATCH
将重置为零。 当 MINOR
递增时,PATCH
将重置为零,而 MAJOR
保持不变。
有些人喜欢用日期作为版本号,如“2019.01.01”,这样也有它的好处,而且很多时候外部版本和内部版本不是一回事。
6 .NET Framework版本
如果只是为了自己或公司创建自定义控件库,当然是根据实际用到的.NET Framework版本选择自定义控件库的版本。就我目前情况来看,我选择了4.5。
7. 代码规范
基本上遵循《.NET设计规范:约定、惯用法与模式》及.Net Core的规范,并且使用FxCop及EditorConfig协助规范代码,参考WindowsCommunityToolkit的设定(但还是有些区别,例如花括号等;后来就越做越多区别)。一些移植过来的代码会使用<CODE>SuppressMessage</CODE>禁止显示警告。
8. 实现原则
我希望尽可能简单的实现一些控件,通过20%的代码解决80%的问题;我更倾向于介绍一种解决问题的思路,而不是提供一个包罗万象、面面俱到的成品。而且更复杂的问题通常都是业务上的需求,保持代码简单更方便其他人修改我的代码并灵活使用。
由于ControlTemplate是很符合开放封闭原则的实现,所以能用ControlTemplate解决的自定义问题我都尽可能留给ControlTemplate解决,而不是通过添加大量属性。
以我的经验来说,添加新功能很容易,移除旧功能会被人打,新功能的添加一定要谨慎。
因为代码总是在WPF、Silverlight、UWP之间移植来移植去,所以我一直更倾向于使用兼容性较好的方案,例如如果使用VisualState的工作量和ControlTemplate.Triggers差不多我就会使用VisualState实现(不过通常ControlTemplate.Triggers都会简单很多)。
不会添加在操作上有“独特创意”的控件。
9. 结语
Kino.Toolkit.Wpf的初衷毕竟是自己用及教学,没有通过充分的测试,如果发现严重的Bug请协助我修复。
按道理所有控件应该都不会拒绝MVVM,不过Sample里面没有用到MVVM模式,如果发现对MVVM不够友好的部分请告知。
示例代码没有使用MVVM模式,这是因为对控件的示例来说MVVM并不是那么直观,一般WPF的教材也都是使用CodeBehind的方式。
最后提一句,对于太过复杂的控件,能让公司花钱买的就尽量花钱买。