ItemsControl: 'L' is for Lookless (a.k.a., "Styles, Templates, andTriggers… Oh My!")
在這片文章中,我們將研究WPF控件如何通過樣式和模板獲取可視化展示。然後我們將看看這些技術是如何應用到ItemsControl類型的控件上。
這篇文章相當長,並且覆蓋很多重要的信息,包括以下話題:
l 無外觀空間模型
l 關於Style
l 關於控件模板
l 關於觸發器
l 模板化ItemsControl
花時間理解這些概念絕對是值得的。我同樣鼓勵你使用kaxaml驗證你學習到的樣式和模板。
1. 無外觀控件模型
當開發者開始學習WPF的時候,他們會非常驚訝WPF控件沒有包含一個硬編碼的外觀。而且默認的外觀是在不同於控件定義程序集的程序集中定義的。這種代碼的邏輯和外觀的隔離使得WPF控件模型非常強大。我們指這種方法爲“無外觀控件”。
爲什麼,或許你會問,這種模型比傳統的將可視元素作爲控件一部分的模型更好嗎?我給你兩個強大的理由:設計和重用性。
如果你學習過WPF和silverlight,你很可能聽說過新的開發者/設計者的工作流程。開發者和設計者可以並行工作來創建強大的應用程序,每個人都能發揮自己的最大優勢。開發人員在實現應用程序的邏輯,同事設計者可以創建引用程序的外觀。在很大程度上,這都是無外觀控件模型的功勞。
第二個重要的理由是隔離設計和控件邏輯提高了可用性。在過去,如果你想一個按鈕有兩種不同的可視外觀,你必須創建兩個單獨的按鈕類,每個按鈕類都要有正確的render代碼。每個類都包含有相同的按鈕邏輯,唯一不同的是兩個按鈕的呈現邏輯。
通過將外觀轉移到XAML中,我們允許一個按鈕在支持點擊的同時,可以有任意多個不同的外觀。開發人員不需要參與到呈現代碼外觀的過程之中。
一個複用性的例子如下,這是一個ListView,每一行都包含了一個樣式名和一個按鈕。每個按鈕間唯一不同的區別是定義按鈕外觀的style。
2. 關於樣式
WPF樣式包含一個集合的屬性值,這些屬性值可以被應用到一個框架元素(通過設置她的style屬性,或者通過隱式樣式,樣式的key使用框架元素的默認樣式key)。一個簡單的例子如下:
<Style TargetType="{x:Type Rectangle}">
<Setter Property="Width"Value="50" />
<Setter Property="Height"Value="50" />
</Style>
那麼我們如何使用這個樣式呢?通常我們將它添加到元素的資源字典中。然後你就可以使用資源鍵來爲子樹中的元素指定樣式了。這是一個例子:
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid.Resources>
<Style x:Key="MyRectangleStyle"
TargetType="{x:Type Rectangle}">
<Setter Property="Width" Value="50" />
<Setter Property="Height" Value="50" />
</Style>
</Grid.Resources>
<StackPanel Orientation="Horizontal">
<Rectangle Style="{StaticResource MyRectangleStyle}" Fill="Red" />
<Rectangle Style="{StaticResource MyRectangleStyle}" Fill="Green" />
<Rectangle Style="{StaticResource MyRectangleStyle}" Fill="Blue" />
<Rectangle Style="{StaticResource MyRectangleStyle}" Fill="Black" />
</StackPanel>
</Grid>
如果我們想隱式設置樣式,那麼把x:Key屬性從樣式聲明中去掉就可以了。如下所示:
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid.Resources>
<Style TargetType="{x:Type Rectangle}">
<Setter Property="Width" Value="50" />
<Setter Property="Height" Value="50" />
</Style>
</Grid.Resources>
<StackPanel Orientation="Horizontal">
<Rectangle Fill="Red" />
<Rectangle Fill="Green" />
<Rectangle Fill="Blue" />
<Rectangle Fill="Black" />
</StackPanel>
</Grid>
你可能感到驚奇這些樣式是如何應用到rectangle元素中的。答案隱藏在框架對資源字典的解析路徑上。如果沒有爲style指定鍵,那麼解析器使用樣式的TargetType作爲他的默認鍵。所以上述樣式等同於以下樣式:
<Style x:Key="{x:Type Rectangle}" TargetType="{x:Type Rectangle}">
<Setter Property="Width" Value="50" />
<Setter Property="Height" Value="50" />
</Style>
這是WPF的一個約定。現在讓我們看看這些樣式特性,甚至一些最有經驗的設計者都不可能知道。
上述樣式可以應用到rectangle上,我們能不能創建一個更通用的樣式,將其應用到任何framework元素上呢?我們只是簡單的不去設置TargetType屬性,就可以創建這麼一個一般樣式。當然這會帶來一些模糊性,因爲我們還不知道對象的類型,WPF不知道如何理解這些屬性,爲了去除這種模糊性,我們必須在setter中指定對象類型,如下:
<Style x:Key="SharedStyle">
<Setter Property="FrameworkElement.Width" Value="50" />
<Setter Property="FrameworkElement.Height" Value="50" />
<Setter Property="Button.IsDefault" Value="True" />
</Style>
現在style可以被應用在不同類型的元素了。實際上,一般性的樣式給了我們設置並不在不同類型元素間共享的屬性,例如,上述樣式可以同時被應用到按鈕和rectangle中。甚至樣式中包含Button.IsDefault屬性的Setter。如下所示:
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid.Resources>
<Style x:Key="SharedStyle">
<Setter Property="FrameworkElement.Width" Value="50" />
<Setter Property="FrameworkElement.Height" Value="50" />
<Setter Property="Button.IsDefault" Value="True" />
</Style>
</Grid.Resources>
<StackPanel Orientation="Horizontal">
<Rectangle Style="{StaticResource SharedStyle}" Fill="Red" />
<Button Style="{StaticResource SharedStyle}">Click Me</Button>
</StackPanel>
</Grid>
3. 關於控件模板
我們已經學習了數據模板和麪板模板,下面我們看一下第三種類型的模板-控件模板。
顧名思義,ControlTemplate用來爲控件定義外觀。所有的WPF控件在每個Windows 主題下都有一個默認的控件模板。這些模板給予了控件默認的外觀。
無外觀控件模型的好處是我們對控件外觀有着完全的控制。我們可以接受默認的控件外觀,也可以定義我們自己的ControlTemplate。下面就是ListBox一個ControlTemplate的例子:
<ControlTemplate x:Key="MyListBoxTemplate" TargetType="{x:Type ListBox}">
<Border Background="White"BorderBrush="Black"
BorderThickness="1" CornerRadius="6">
<ScrollViewer Margin="4">
<ItemsPresenter />
</ScrollViewer>
</Border>
</ControlTemplate>
(此控件模板非常簡單,不再詳述)
與style類似,ControlTemplate也有一個TargetType屬性。這個屬性必須被設置。如果你發現你的模板不能按照你希望的方法展示,確保你設置了ControlTemplate的TargetType屬性。
現在我們來應用ListBox的自定義模板,將其指定爲ListBox的Template屬性值,如下:
<ListBox Width="80" Height="200"
Template="{StaticResource MyListBoxTemplate}">
<Rectangle Width="50" Height="50" Fill="Red" />
<Rectangle Width="50" Height="50" Fill="Green" />
<Rectangle Width="50" Height="50" Fill="Blue" />
<Rectangle Width="50" Height="50" Fill="Black" />
</ListBox>
前面的例子中,我們使用Template屬性設置ListBox的模板,這不是常用的方式。模板通常作爲樣式的一部分出現。
樣式包含一個Setter對象的集合。對於一個控件樣式,應該包含一個Setter用來設置她的Template屬性,一個簡單的ListBox樣式如下:
<Style TargetType="{x:Type ListBox}">
<Setter Property="Background"Value="LightGray" />
<Setter Property="BorderThickness"Value="2" />
<Setter Property="HorizontalContentAlignment"Value="Center" />
<Setter Property="Padding"Value="5" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBox}">
<Border Background="White"BorderBrush="Black"
BorderThickness="1" CornerRadius="6">
<ScrollViewer Margin="4">
<ItemsPresenter />
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
假定我們將以上樣式添加到應用程序資源中,那麼所有的應用程序中所有的ListBox都能隱士使用我們的控件模板。所以現在我們可以添加如下ListBox到我們的UI中:
<ListBox Width="100" Height="200">
<Rectangle Width="50" Height="50" Fill="Red" />
<Rectangle Width="50" Height="50" Fill="Green" />
<Rectangle Width="50" Height="50" Fill="Blue" />
<Rectangle Width="50" Height="50" Fill="Black" />
</ListBox>
在我們的例子中有一個問題,就是一些屬性值是硬編碼的。其他屬性也沒有綁定到模板內的元素屬性。我們需要使用TemplateBinding來解決這個問題。
TemplateBinding是一個輕量級綁定,用來把控件屬性值連接到控件模板內元素的屬性上。在我們的ListBox示例中,我們可以添加這樣的綁定,如下所示:
<Style TargetType="{x:Type ListBox}">
<Setter Property="Background"Value="LightGray" />
<Setter Property="BorderThickness"Value="2" />
<Setter Property="HorizontalContentAlignment"Value="Center" />
<Setter Property="Padding"Value="5" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBox}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="6">
<ScrollViewer Margin="{TemplateBinding Padding}">
<ItemsPresenter />
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
現在所有的屬性都被連接到模板內元素的屬性。因此這些屬性真是反映了控件的外觀展示。
4. 關於觸發器
觸發器就是當給定條件滿足的時候應用一個集合的Setter對象。WPF包含三種類型的Trigger(也可以說是屬性Trigger),DataTrigger和EventTrigger。也可以使用MultiTrigger和MultiDataTrigger來響應多個條件。
每個Style,ControlTemplate,DataTemplate都有一個Trigger集合,FrameworkElement也有一個Trigger集合,但是隻能添加EventTrigger。讓我們看一個簡單的示例,下面是一個Button的style
<Style TargetType="{x:Type Button}">
<Style.Triggers>
<Trigger Property="IsMouseOver"Value="True">
<Setter Property="Opacity"Value="0.7" />
<Setter Property="TextBlock.FontWeight"Value="Bold" />
</Trigger>
</Style.Triggers>
</Style>
(樣式非常簡單,不再解釋)
由上述樣式看出,trigger非常直觀。你通過閱讀標記就可以瞭解大概的情況。所以,我們不用花太多時間來解釋它的用法。我們只在不同的場景裏看看它複雜的地方。
DataTrigger允許你通過綁定在一個數據項屬性變化時引發。這在DataTemplate中用的非常普遍。但是不要小看DataTrigger在其他場景中的應用。例如,有另外一個對象的屬性引發一個觸發器也是非常有用的,如下:
<DataTemplate x:Key="MyItemTemplate">
<Border x:Name="root"BorderThickness="2" CornerRadius="6">
<TextBlock Margin="4" Text="{Binding XPath=@First}" />
</Border>
<DataTemplate.Triggers>
<DataTrigger Value="True"Binding="{Binding Path=IsSelected,
RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}">
<Setter TargetName="root"Property="BorderBrush" Value="Pink" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
在這個例子,我們創建了DataTrigger,但是很大程度上,它類似於屬性觸發器。我們通過FindAncester綁定來查找觸發源。當項在被選擇的時候,會顯示一個粉色的邊框。如果我們想爲一個女性項被選擇的時候顯示粉色邊框,而男性項被選擇的時候顯示一個藍色的邊框,我們可以添加第二個Trigger,如下:
<DataTemplate x:Key="MyItemTemplate">
<Border x:Name="root"BorderThickness="2" CornerRadius="6">
<TextBlock Margin="4" Text="{Binding XPath=@First}" />
</Border>
<DataTemplate.Triggers>
<DataTrigger Value="True"Binding="{Binding Path=IsSelected,
RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}">
<Setter TargetName="root"Property="BorderBrush" Value="Pink" />
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding XPath=@Gender}" Value="Male" />
<Condition Value="True"Binding="{Binding Path=IsSelected,
RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" />
</MultiDataTrigger.Conditions>
<Setter TargetName="root"Property="BorderBrush" Value="Blue" />
</MultiDataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
上例說明了觸發器的另個重要的特點,觸發器中setter對象的順序非常重要。當多個觸發器都被觸發是,所有的setter都被應用,而且以他們在Triggers集合中顯示的順序所應用。換句話說,如果多個setter都有相同的目標屬性,那麼最後一個setter纔會贏。
最後一點要說是,如何通過MultiDataTrigger聯合屬性觸發器和數據觸發器。有時候,這非常方便,不過人們通常會忘記使用DataTrigger可以被當作Property trigger使用。
還有一個trigger是EventTrigger,事件觸發器通常被用於在事件發生時引發一個響應動作。下面是一個使用事件觸發器的按鈕,當button第一次load的時候,會對透明度做一個動畫效果。
<Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Button Opacity="0" Content="My Button">
<Button.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard Name="MyBeginAction">
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
Duration="0:0:1"BeginTime="0:0:0.25" To="1.0" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
</Canvas>
再一次說明,FrameworkElement的觸發器集合只能添加EventTrigger,如果要使用其他兩種觸發器,那麼就必須通過樣式或者模板使用。
注意,你不能對標準的CLR事件使用觸發器,因爲事件觸發器需要WPF事件路由引擎的支持。
當使用屬性觸發器或者數據觸發器的時候,條件變化引發觸發器對目標值的設置。所以,當條件滿足的時候,setter對象被應用到目標屬性上,當條件不滿足的時候,目標值將會恢復到trigger之前的值。但是事件觸發器沒有這樣的概念。事件是瞬時的,所以EventTrigger沒有包含一個Setter集合。而是包含了響應事件的動作。該動作允許你控制動畫的執行。
如果你仍然希望通過事件觸發器來設置屬性值。即使你沒有setter你同樣可以做到這個。使用對象動畫的DiscreteObjectKeyFrame即可,如下:
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib">
<Grid.Triggers>
<EventTrigger RoutedEvent="CheckBox.Checked">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="tb"
Storyboard.TargetProperty="Text" Duration="0:0:0.1">
<DiscreteObjectKeyFrame Value="Thanks!
Thatfelt great!!" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<EventTrigger RoutedEvent="CheckBox.Unchecked">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0:0:0.1"
Storyboard.TargetName="tb"
Storyboard.TargetProperty="Text">
<DiscreteObjectKeyFrame Value="Ohyeah!
I'm outta here!" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Duration="0:0:0.1"
Storyboard.TargetProperty="IsHitTestVisible">
<DiscreteObjectKeyFrame>
<DiscreteObjectKeyFrame.Value>
<sys:Boolean>False</sys:Boolean>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<DoubleAnimation Duration="0:0:0.5"To="0"
Storyboard.TargetProperty="Opacity"
BeginTime="0:0:2"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Grid.Triggers>
<ToggleButton Width="100"Height="100">
<TextBlock x:Name="tb" Text="Toggle Me, Please!"
TextAlignment="Center" TextWrapping="Wrap" />
</ToggleButton>
</Grid>
備註:使用離散動畫幀設置屬性非常有用。特別是在silverlight中,因爲silverlight不支持數據觸發器和屬性觸發器。
值得注意的是,DataTrigger和Tigger也可以啓動一個動畫。你只需要設置他們的EnterActions和ExitActions。如下所示:
<StackPanel Orientation="Horizontal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel.Resources>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBoxBase}">
<Grid>
<Rectangle Margin="1" Fill="#FFF" />
<Rectangle Name="DisabledBackground" Margin="1"
Fill="#CCC" RenderTransformOrigin="1,0.5">
<Rectangle.RenderTransform>
<ScaleTransform ScaleX="0"/>
</Rectangle.RenderTransform>
</Rectangle>
<Border Name="Border" CornerRadius="2" Padding="2"
BorderThickness="1" BorderBrush="#888">
<ScrollViewer Margin="0" x:Name="PART_ContentHost" />
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Border" Property="BorderBrush" Value="#AAA"/>
<Setter Property="Foreground" Value="#888"/>
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard TargetName="DisabledBackground"
TargetProperty="RenderTransform.ScaleX">
<DoubleAnimation To="1" Duration="0:0:0.7"
DecelerationRatio="0.5" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard TargetName="DisabledBackground"
TargetProperty="RenderTransform.ScaleX">
<DoubleAnimation To="0" Duration="0:0:0.7"
DecelerationRatio="0.5" />
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</StackPanel.Resources>
<CheckBox Name="MyCheckBox"FontWeight="Bold" Margin="0,6,4,0"
VerticalAlignment="Top" Content="Print To File:" />
<TextBox VerticalAlignment="Top"Margin="0,3,0,0" Text="C:\MyFile.prn"
Width="200" IsEnabled="{Binding IsChecked, ElementName=MyCheckBox}" />
</StackPanel>
爲了瞭解無外觀模式是如何工作的,我推薦你花點時間來查看一下WPF本地控件的樣式和模板。他們與Windows SDK和Blend一起發佈。當安裝SDK的時候,你必須選擇安裝.NET Framework3.0示例。那麼你可以在下面的目錄中找到他:%ProgramFiles%\MicrosoftSDKs\Windows\v6.0 \Samples\WPFSamples.zip. 如果安裝了Blend ,可在如下目錄中找到: “%ProgramFiles%\MicrosoftExpression\Blend <version>\SystemThemes”.
5. 模板化ItemsControl
這是一個ItemsControl系列,所以我們需要一點時間來研究ItemsControl的無外觀本性。不過下面的概念也可以應用到其他控件。
從無外觀課程中學到的
在介紹無外觀模型的時候,我有一個必須要說的故事…
早期的WPF平臺包含了一個ItemsControl但是沒來沒有發佈。它是RadioButtonList,從名字可以瞭解,這是一個互斥項列表,每個選項都是一個radiobutton。第一眼看上去,這是一個非常有用的控件,是嗎?我們也是這麼想的。不過當我們在POC上宣傳的時候,有人指出,我們已經有了一個互斥項選擇控件,那就是單選項的ListBox。
無外觀控件的精神需要我們去適應。很明顯,即使是這個模式的架構師有時候也不能完全領會。
下面是的ListBox作爲RadioButtonList樣式:
<Window Title="RadioButtonList Sample" Width="500" Height="180"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
Topmost="{Binding SelectedItem, ElementName=TopmostSelector}"
WindowStyle="{Binding SelectedItem, ElementName=WindowStyleSelector}">
<Window.Resources>
<ObjectDataProvider x:Key="WindowStyles"MethodName="GetValues"
ObjectType="{x:Type sys:Enum}" >
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="WindowStyle"/>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
<Style x:Key="RadioButtonList"TargetType="{x:Type ListBox}">
<Setter Property="BorderBrush"Value="{x:Null}" />
<Setter Property="BorderThickness"Value="0" />
<Setter Property="Tag"Value="Vertical" />
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<StackPanel Orientation="{Binding Tag,
RelativeSource={RelativeSource AncestorType={x:Type ListBox}}}" />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Margin"Value="6,2" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Border Background="Transparent">
<RadioButton Focusable="False"
IsHitTestVisible="False"
IsChecked="{TemplateBinding IsSelected}">
<ContentPresenter />
</RadioButton>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<StackPanel Margin="10">
<TextBlock FontWeight="Bold">Topmost:</TextBlock>
<ListBox Name="TopmostSelector"SelectedIndex="1" Margin="10,0,0,20"
Style="{StaticResource RadioButtonList}" Tag="Vertical">
<sys:Boolean>True</sys:Boolean>
<sys:Boolean>False</sys:Boolean>
</ListBox>
<TextBlock FontWeight="Bold">WindowStyle:</TextBlock>
<ListBox Name="WindowStyleSelector"SelectedIndex="1" Margin="10,0,0,0"
Style="{StaticResource RadioButtonList}" Tag="Horizontal"
ItemsSource="{Binding Source={StaticResource WindowStyles}}" />
</StackPanel>
</Window>
下面是一些ListBox的圖片示例:
自然,ListBox也可以顯示一個RadioButton的列表:
在P是面板中介紹的那個非常酷的自定義面板,聯合ListBox可以用來研究實體關係:
有一個TreeGraph控件也是使用ListBox,他從ListBox繼承並添加了額外的功能:
值得注意的是,ListView也是從ListBox繼承的,ListView其實是一個多列的ListBox:
無外觀模式應該用來創建更好的用戶體驗,我看多很多次有人在視頻中演示旋轉的按鈕,這應該避免。
項面板
你總是能在ItemsControl中發現ItemsPresenter。前面,我們學到了ItemsPresenter保留了項宿主的狀態。通過在控件模板中包含ItemsPresenter並且在樣式中指定ItemsPanelTemplate屬性,我們不同重新定義控件模板就可以切換面板。ItemsPanelTemplate將會在ItemsPresenter中被展開成一個面板用來容納項容器。而使用IsItemsHost是不推薦的。
ToolBar控件就是一個使用IsItemsHost的例子。不過,我認爲從ItemsControl繼承並且依賴一些特定的已知模板元素是可以的。不過,作爲一個控件開發者,你需要完整的記錄你的控件需求,雖然這樣做降低了無外觀模式的能力。設計者也需要在設計的時候關注到這些限制。
其他已知部分[TemplatePart]
有時候ItemsControl會依賴其他元素,讓我們以ComboBox爲例:
ComboBox是一個單選控件,整個列表顯示在一個下拉列表中。ComboBox的模板包含一個TextBox和一個Popup。你可能會像,TextBox顯示選擇項,而Popup包含ItemsPresenter,所以他能展示所有可用項。
ComboBox包含了針對Popup控件的專門邏輯。雖然使用“template part”降低了控件的無外觀性,但是隻要你完整記錄的需求,那麼也是允許的。
這自然引發一個問題,ComboBox是如何知道在模板裏有一個Popup呢?一個方法就是在可視樹中查找。這不是一個好方法,爲了更好的處理這種情況,framework使用了一個命名決定。就是對模板中的已知控件用PART_前綴來命名。對於ComboBox中的Popup來說,這個Popup就是“PART_Popup”。如下:
<Popup Name="PART_Popup" AllowsTransparency="True" Placement="Bottom">
...
</Popup>
雖然有了名字,但是我們怎麼使用它呢?framework提供了一個方法讓我們能夠找到它。通過重寫OnApplyTemplate方法,當一個模板被展開的時候,控件可以獲得通知。通過調用GetTemplateChild方法,可以獲得模板中指定名稱的控件,如下:
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_popup = GetTemplateChild("PART_Popup")as Popup;
}
這麼做,顯然讓設計者變得困難。所以framework提供了TemplatePart自定義屬性來裝飾你的控件類。用來提示設計者模板控件中必須有什麼樣的元素存在。如下所示:
[TemplatePart(Name="PART_EditableTextBox",Type=typeof(TextBox))]
[TemplatePart(Name="PART_Popup",Type=typeof(Popup))]
public class ComboBox : Selector
{
...
}
這種工作方式對framework中的所有控件都是有效的。設計者在設計控件外觀的時候,他們需要了解這些TemplatePart。
保持松耦合
我對自定義控件作者的一點建議就是在控件和模板之間保持一個松的契約。這意味着當有的元素沒有在模板中出現的時候,控件不會崩潰。進一步講,控件可以優雅的降級。ItemsControl就是這麼做的。如果你重新定義了ItemsControl的模板並且遺失了ItemsPresenter,ItemsControl不會報錯,它只是什麼也不顯示。測試一下你的控件是不是也在元素丟失的時候還能正常工作,能夠得知你的控件是不是真的支持無外觀模式。