1. 程式人生 > >[WPF自定義控制元件] 開始一個自定義控制元件庫專案

[WPF自定義控制元件] 開始一個自定義控制元件庫專案

1. 目標

我實現了一個自定義控制元件庫,並且打算用這個控制元件庫作例子寫一些部落格。這個控制元件庫主要目標是用於教學,希望通過這些部落格初學者可以學會為自己或公司建立自定義控制元件,並且對WPF有更深入的瞭解。

控制元件庫已放在Github上,並且也以釋出到NuGet。

現階段我的目標是實現一些簡單的控制元件,由於我並不是打算重複造輪子,所以我會挑些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 遞增時,MINORPATCH 將重置為零。 當 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的方式。

最後提一句,對於太過複雜的控制元件,能讓公司花錢買的就儘量花錢買。

10. 引用

Github
Nu