歡迎訪問Heroius博客:崩潰的腦殼查看文章原文!
前言
相信不少學習WPF和Silverlight的同學們都出於Winform的習慣,希望能夠在新展示層框架中實現控件的繼承。本文就是說明如何實現這一點。
但是在正文開始之前,必須要指明,一般情況下,在WPF/SL中並不推薦使用自定義控件或控件繼承(當然,使用模板生成的Window, UserControl, Page不在此限),因為基於XAML的前台設計語言本身具有豐富的表現能力,且框架支持樣式(Style)、模板(Template)等控制外觀和行為的方式—-這些特性足以構建出任何形式的界面UI。反過來說,創建自定義控件或實現控件繼承在WPF/SL中不僅不受推崇,其實現難度也要比Winform中大。
那么當什么時候才不得不使用自定義控件呢?就是這個控件需要在多處實例化使用,而本身內容較為復雜時。相比較而言,控件的繼承使用的情況就更少了,它使用的必要性一般要滿足如下幾條:
- 只在原控件不滿足新需求,但同時又有可資利用的價值;
- 新控件需要直接訪問原控件成員(否則在新控件中包含原控件即可,不必繼承);
- 新控件同樣需要在多處使用。
如果你面臨的情況滿足這幾個條件,閱讀本文可能會提供幫助。
問題分析
在WPF/SL中實現控件繼承之所以會比Winform困難,是因為在底層框架設計上WPF/SL將表示層徹底從邏輯中分離了出去,控件外觀幾乎均有XAML標記定義,而在對控件進行繼承時,XAML部分對應的類型成員是無法被繼承的。
以WPF為例,使用UserControl模板新建用戶控件,得到一個.cs文件和一個.xaml文件。注意在.cs文件中類定義有partial修飾,說明是分部類,代碼中的InitializeComponent函數即是在另外一部分代碼(設計器生成代碼)中定義的。這部分由設計器生成的代碼和Winform中設計器的代碼相差很大。
在生成文件夾中可以看到設計器生成的.g.i.cs文件,其中包含對應於XAML內命名成員的相應變量的定義,以及InitializeComponent方法實現。
可見於Winform設計器代碼相比,其中不包含C#代碼形式的控件初始化邏輯,所有界面表達均在xaml文件中,代碼通過System.Windows.Application.LoadComponent方法從xaml文件實例化。
在.xaml文件中,可見<UserControl>標簽及其屬性x:class,此兩者指明了XAML文檔對應的類型信息,其中根元素是當前類型的基類(UserControl),x:class屬性指定當前類型(UserControl1)。
注意到Application.LoadComponent方法包含兩個重載:
- object (Uri) – 接受xaml資源的定位符,返回其根元素決定的實例;
- void (object, Uri) – 接受根元素類型實例和資源定位符。
自動生成代碼采用了第2個重載,並傳遞當前類實例作為第一個參數,也就是說,XAML加載得到了拓展的UserControl1類型。
這種在xaml中指定類型信息的類(UserControl1)被稱為是“由XAML定義的”。而XAML渲染器無法識別由XAML定義的根類型,也就是說當控件繼承時,若在子類型控件(如UserControl2)設計器中指定其為根元素時,編譯過程將失敗。
但假如子類型不包含XAML代碼,如新建Class1繼承自UserControl1,則沒有問題。
現在的問題是,在設計控件,尤其是結構較復雜時,我們往往需要借助設計器,這就要求必須使用XAML代碼,這種情況應該如何應對呢?
XAML UserControl的繼承
命題:兩個帶有設計界面的類型UserControl1和UserControl2,其中后者繼承於前者。
思路:
- 對於xaml代碼,利用Application.LoadComponent方法可獲取根元素決定的實例;
- UserControl屬於Content Control,其顯示內容由Content決定;
- 基於以上兩點,分離控件xaml部分,並修改為以某FrameworkElement為根,如此可得到用於設置Content屬性的可視化內容。
UserControl2繼承代碼修改
修改UserControl2的代碼,使其繼承自UserControl1
1
2
3
4
|
public partial class UserControl2 : UserControl1
{
//...
}
|
修改xaml代碼,將根元素設置為基類,注意引用本地程序集命名空間
1
2
3
4
5
6
7
8
9
10
11
12
|
<ci:UserControl1 x:Class="MyNamespace.UserControl2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
xmlns:ci="clr-namespace:ControlInherit"
d:DesignHeight="300" d:DesignWidth="300">
<Grid x:Name="layoutRoot2">
<Label Content="UC2 xaml" Foreground="LightGreen" HorizontalAlignment="Left" VerticalAlignment="Top"/>
</Grid>
</ci:UserControl1>
|
UserControl1 xaml和代碼分離
UserControl1控件包含的.xaml和.xaml.cs文件由VS管理,為了使兩者分開,需要重命名。先將項從項目中移除,分別重命名:
- UserControl1.xaml -> UserControl1_skin.xaml
- UserControl1.xaml.cs -> UserControl1.cs
重新加載到項目中,修改xaml文件根元素,使用Grid代替UserControl
1
2
3
4
5
6
7
8
9
|
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid.Resources>
<ResourceDictionary Source="/MyAssembly;component/Dictionary1.xaml"/>
</Grid.Resources>
<Grid>
<Button x:Name="BtnMe" Content="Button" Style="{StaticResource ResourceKey=BtnStyle}"/>
</Grid>
</Grid>
|
移除UserControl.cs中類定義前的partial修飾符,並手動添加InitializeComponent函數,在其中利用Application.LoadComponent的第一個重載獲取如上修改之后xaml編譯得到的Grid實例,將其設置給Content:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public void InitializeComponent()
{
if (_contentLoaded)
{
return;
}
_contentLoaded = true;
System.Uri resourceLocater = new System.Uri("/MyAssembly;component/usercontrol1_skin.xaml", System.UriKind.Relative);
var root = Application.LoadComponent(resourceLocater);
this.Content = root;
//使用FindName方法獲取實例
this.BtnMe = ((System.Windows.Controls.Button)(((Grid)root).FindName("BtnMe")));
}
private bool _contentLoaded;
internal System.Windows.Controls.Button BtnMe;
|
UserControl2的代碼調整
為避免UserControl2自動生成代碼覆蓋基類的Content內容,在調用UserControl2的InitializeComponent函數之前需要獲取基類Content,即上文中的Grid實例,並將其插入到當前UserControl2的最下層。
1
2
3
4
5
6
|
public UserControl2()
{
var root = (Grid)base.Content;
InitializeComponent();
layoutRoot2.Children.Insert(0, root);
}
|
此時在主窗體中拖放UserControl2,程序運行效果如下:
其他注意事項
關於Silverlight
Silverlight中不包含 Application.LoadComponent的第一個重載,可事先創建Grid實例,之后將其作為參數調用 Application.LoadComponent第二重載,效果和WPF一樣。
關於界面設計
若UserControl1界面中有交互內容,設計UserControl2時需要注意避讓。
實際上,完全可以通過代碼控制界面元素的布局,例如可以嘗試將UserControl2構造函數的代碼改成如下內容:
1
2
3
4
5
6
7
8
9
10
|
public UserControl2()
{
//InitializeComponent();
var root = (Grid)base.Content;
root.RowDefinitions.Add(new RowDefinition());
root.RowDefinitions.Add(new RowDefinition());
var lb = new TextBlock() { Text = "this is uc2" };
Grid.SetRow(lb, 4);
root.Children.Add(lb);
}
|
示例代碼
訪問 崩潰的腦殼 文章頁面底部以獲取百度網盤地址和提取密碼