用戶控件的目標是提供增補控件模板的設計表面,提供一種定義控件的快速方法,代價是失去了未來的靈活性。若是喜歡用戶控件的功能,但須要修改使其可視化外觀,使用這種方法就有問題了。例如,設想但願使用相同的顏色拾取器,但但願使用不一樣的「皮膚」,將其更好地融合到已有的應用程序窗口中。能夠經過樣式來改變用戶控件的某些方面,但該控件的一些部分是在內部鎖定,並硬編碼到標記中。例如,沒法將預覽矩形移動到滑動條的左邊。ide
解決方法是建立無外觀控件——繼承自控件基類,但沒有設計表面的控件。相反,這個控件將其標記放到默認模板中,可替換默認模板而不會影響控件邏輯。函數
1、修改顏色拾取器的代碼佈局
將顏色拾取器改爲無外觀控件並不難。第一步很容易——只須要改變類的聲明,以下所示:this
public class ColorPicker:System.Windows.Controls.Control { }
在這個示例中,ColorPicker類繼承自Control類。繼承自FrameworkElement類是不合適的,由於顏色拾取器容許與用戶進行交互,並且其餘高級的類不能準確地描述顏色拾取器的行爲。例如,顏色拾取器不容許在內部嵌套其餘內容,因此繼承自ContentControl類也是不合適的。編碼
ColorPicker類中的代碼與用於用戶控件的代碼是相同的(除了必須刪除構造函數中的InitializeComponent()方法調用)。可以使用相同的方法定義依賴項屬性和路由事件。惟一的區別是須要通知WPF,將爲控件類提供新樣式。該樣式將提供新的控件模板(若是不執行該步驟,將繼續使用在基類中定義的模板)。spa
爲通知WPF正在提供新的樣式,須要在子彈女工藝控件類的靜態構造函數中調用OverrideMetadata()方法。須要在DefaultStyleKeyProperty屬性上調用該方法,該屬性是爲自定義控件定義默認樣式的依賴性屬性。須要的代碼以下所示:設計
DefaultStyleKeyProperty.OverrideMetadata(typeof(ColorPicker), new FrameworkPropertyMetadata(typeof(ColorPicker)));
若是但願使用其餘控件類的模板,可提供不一樣的類型,但幾乎老是爲每一個自定義控件建立特定的樣式。雙向綁定
2、修改顏色拾取器的標記code
添加對OverrideMetadata()方法的調用後,只須要插入正確的樣式。須要將樣式放在名爲generic.xaml的資源字典中,該資源字典必須放在項目文件夾的Themes子文件夾中。這樣,該樣式就會被識別爲自定義控件的默認樣式。下面列出添加generic.xaml文件的具體步驟:component
(1)在Solution Explorer中右鍵類庫項目,並選擇Add|New Folder菜單項。
(2)將新建文件夾命名爲Themes。
(3)右擊Themes文件夾,並選擇Add|New Item菜單項。
(4)在Add New Item對話框中選擇資源字典,輸入名稱generic.xaml,並單擊Add按鈕。
下圖顯示了Themes文件夾中的generic.xaml文件。
一般,自定義控件庫會包含幾個控件。爲了保持它們的樣式相互獨立以便編輯,generic.xaml文件一般使用資源字典合併功能。下面是標記顯示了generic.xaml文件,該文件從ColorPicker.xaml資源字典中提取資源,該資源字典位於CustomControls控件庫的Themes文件夾中:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="/CustomControls;component/Themes/ColorPicker.xaml"> </ResourceDictionary> </ResourceDictionary.MergedDictionaries> </ResourceDictionary>
自定義的控件樣式必須使用TargetType特性來將自身自動關聯到顏色拾取器。下面是ColorPicker.xaml文件中標記的基本結構:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:CustomControls"> <Style TargetType="{x:Type local:ColorPicker}"> ... </Style> </ResourceDictionary>
可以使用樣式設置控件類中的任意屬性(不管是繼承自基類的屬性仍是新增屬性)。但在此,樣式最有用的任務是應用新目標,新目標定義了控件的默承認視化外觀。
很容易就能將普通標記(如顏色拾取器使用的標記)轉換到控件目標中。但要注意如下幾點:
遵循上面幾點,可爲顏色拾取器建立如下模板:
<Style TargetType="{x:Type local:ColorPicker}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:ColorPicker}"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <Slider Minimum="0" Maximum="255" Margin="{TemplateBinding Padding}" Value="{Binding Path=Red, RelativeSource={RelativeSource TemplatedParent}}"/> <Slider Grid.Row="1" Minimum="0" Maximum="255" Margin="{TemplateBinding Padding}" Value="{Binding Path=Green, RelativeSource={RelativeSource TemplatedParent}}"/> <Slider Grid.Row="2" Minimum="0" Maximum="255" Margin="{TemplateBinding Padding}" Value="{Binding Path=Blue, RelativeSource={RelativeSource TemplatedParent}}"/> <Rectangle Grid.Column="1" Grid.RowSpan="3" Margin="{TemplateBinding Padding}" Width="50" Stroke="Black" StrokeThickness="1"> <Rectangle.Fill> <SolidColorBrush Color="{Binding Path=Color,RelativeSource={RelativeSource TemplatedParent}}"></SolidColorBrush> </Rectangle.Fill> </Rectangle> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style>
正如上面看到的,本例已用TemplateBinding擴展提到一些綁定表達式。其餘一些綁定表達式仍使用Binding擴展,但將RelativeSource設置爲指向模板的父元素(自定義控件)。儘管TemplateBinding和將RelativeSource屬性設置爲TemplatedParent值得Binding的做用相同——從自定義控件的屬性中提取數據——可是使用量級更輕的TemplateBinding老是合適的。若是須要雙向綁定(與滑動條同樣)或綁定到繼承自Freezable的類(如SolidColorBrush類)的屬性,TemplateBinding就不能工做了。
3、精簡控件模板
經過上面設計,顏色拾取器控件模板填充了須要的所有內容,可按與使用顏色拾取器相同的方式來使用。然而,仍可經過移除一些細節來簡化模板。
如今,全部但願提供自定義模板的控件使用這必須添加大量的綁定表達式,已確保控件可以繼續工做。這並不難,可是很繁瑣。另外一種選擇是,在控件自身的初始化代碼中配置全部綁定表達式。這樣,模板就不須要指定這些細節了。
一、添加部件名稱
爲了讓這一系統可以工做,代碼要能找到所需的元素。WPF控件經過名稱定爲它們須要的元素。因此,元素的名稱變成自定義控件公有接口的一部分,並且須要恰當的描述性名稱。根據約定,這些名稱以PART_開頭,後跟元素名稱。元素名稱的首字母要大寫,就像數學名稱。對於須要的元素名稱,PART_RedSlider是合適的選擇,而PART_sldRed、PART_redSlider以及RedSlider等名稱都不合適。
例如,下面的標記演示瞭如何經過刪除三個滾動條的Value數學的綁定表達式,併爲三個滑動條添加PART_名稱,從而爲經過代碼設置綁定作好準備。
<Slider Name="PART_RedSlider" Minimum="0" Maximum="255" Margin="{TemplateBinding Padding}" /> <Slider Name="PART_GreemSlider" Grid.Row="1" Minimum="0" Maximum="255" Margin="{TemplateBinding Padding}" /> <Slider Name="PART_BlueSlider" Grid.Row="2" Minimum="0" Maximum="255" Margin="{TemplateBinding Padding}" />
注意,Margin數學仍使用綁定表達式添加內邊距,但這是一個可選的細節,能夠很容易地從自定義模板中去掉該細節(可選擇硬編碼內邊距或者使用不一樣的佈局),
爲確保得到更大的靈活性,這是沒有爲Rectangle元素提供名稱,而是爲其內部的SolidColorBrush指定了名稱。這樣,可根據模板爲顏色預覽功能使用任何形狀或任意元素。
<Rectangle Grid.Column="1" Grid.RowSpan="3" Margin="{TemplateBinding Padding}" Width="50" Stroke="Black" StrokeThickness="1"> <Rectangle.Fill> <SolidColorBrush x:Name="PART_PreviewBrush"></SolidColorBrush> </Rectangle.Fill> </Rectangle>
二、操做模板部件
在初始化控件後,可鏈接綁定表達式,但有一種更好的方法。WPF有一個專用的OnApplyTemplate()方法,若是須要在模板中查找元素並關聯事件處理程序或添加數據綁定表達式,應重寫該方法。在該方法中,能夠經過GetTemplateChild()方法查找所需的元素。
若是沒有找到但願處理的元素,推薦的模式就不起做用。也可添加代碼來檢索該元素,若是元素存在,在檢查類型是否正確;若是類型不正確,就引起異常。
下面的代碼演示了OnApplyTemplate()方法使用:
public override void OnApplyTemplate() { base.OnApplyTemplate(); RangeBase slider = GetTemplateChild("PART_RedSlider") as RangeBase; if (slider != null) { Binding binding = new Binding("Red"); binding.Source = this; binding.Mode = BindingMode.TwoWay; slider.SetBinding(RangeBase.ValueProperty, binding); } slider = GetTemplateChild("PART_GreenSlider") as RangeBase; if (slider != null) { Binding binding = new Binding("Green"); binding.Source = this; binding.Mode = BindingMode.TwoWay; slider.SetBinding(RangeBase.ValueProperty, binding); } slider = GetTemplateChild("PART_BlueSlider") as RangeBase; if (slider != null) { Binding binding = new Binding("Blue"); binding.Source = this; binding.Mode = BindingMode.TwoWay; slider.SetBinding(RangeBase.ValueProperty, binding); } SolidColorBrush brush = GetTemplateChild("PART_PreviewBrush") as SolidColorBrush; if (brush != null) { Binding binding = new Binding("Color"); binding.Source = brush; binding.Mode = BindingMode.OneWayToSource; this.SetBinding(ColorPicker.ColorProperty, binding); } }
注意,上面代碼使用的是System.Windows.Controls.Primitives.RangeBase類(Slider類繼承自該類)而不是Slider類。由於RangeBase類提供了須要的最小功能——在本例中是中Value屬性。經過儘量提升代碼的通用性,控件使用者可得到更大自由。例如,如今可提供自定義模板,使用不一樣的派生自RangeBase類的控件代替顏色滑動條。
綁定SolidColorBrush畫刷的代碼稍有區別,由於SolidColorBrush畫刷美譽包含SetBinding()方法(該方法是在FrameworkElement類中定義的)。一個比較容易得變通方法是爲ColorPicker.Color屬性建立綁定表達式,使用指向源方向的單向綁定。這樣,當顏色拾取器的顏色改變後,將自動更新畫刷。
爲查看這種設計變化的優勢,須要建立一個使用顏色拾取器的控件,並提供一個新的控件模板。
三、記錄模板部件
對於上面的示例,還有最後一處應予改進。良好的設計指導原則建議爲控件聲明添加TemplatePart特性,以記錄在控件模板中使用了哪些部件名稱,以及爲每一個部件使用了什麼類型的控件。從技術角度看,這一步不是必須的,但該文檔可爲其餘使用自定義類的用戶提供幫助。
下面是應當爲ColorPicker控件類添加的TemplatePart特性:
[TemplatePart(Name = "PART_RedSlider", Type = typeof(RangeBase))] [TemplatePart(Name = "PART_BlueSlider", Type = typeof(RangeBase))] [TemplatePart(Name = "PART_GreenSlider", Type = typeof(RangeBase))] [TemplatePart(Name = "PART_PreviewBrush", Type = typeof(SolidColorBrush))] public class ColorPicker:System.Windows.Controls.Control { }
本實例源碼:CustomControlsV2.0.zip