1. 目標 我實現了一個自定義控制項庫,並且打算用這個控制項庫作例子寫一些博客。這個控制項庫主要目標是用於教學,希望通過這些博客初學者可以學會為自己或公司創建自定義控制項,並且對WPF有更深入的瞭解。 控制項庫已放在 "Github" 上,並且也以發佈到 "NuGet" 。 現階段我的目標是實現一些簡單的控制項 ...
1. 目標
我實現了一個自定義控制項庫,並且打算用這個控制項庫作例子寫一些博客。這個控制項庫主要目標是用於教學,希望通過這些博客初學者可以學會為自己或公司創建自定義控制項,並且對WPF有更深入的瞭解。
現階段我的目標是實現一些簡單的控制項,由於我並不是打算重覆造輪子,所以我會挑些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的設定(但還是有些區別,例如花括弧等;後來就越做越多區別)。一些移植過來的代碼會使用SuppressMessage
禁止顯示警告。
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的方式。
最後提一句,對於太過複雜的控制項,能讓公司花錢買的就儘量花錢買。