WPF 你真的會(huì)寫 XAML 嗎?淺談 ControlTemplate 、DataTemplate 和其它 Template
WPF 你真的會(huì)寫 XAML 嗎?淺談 ControlTemplate 、DataTemplate 和其它 Template
本文希望從寫死的代碼慢慢引入 WPF 的一些機(jī)制。
一、Button 難題
我們想要修改 Button 的背景色但是效果非常不理想,默認(rèn)的 Button 樣式是完全無(wú)法給大家看的,改造 Button 的方法是借助 Style 在 Template 中自定義 ControlTemplate(Style 并不關(guān)鍵)。
<Style x:Key="Button_Test_Style" TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid Width="100" Height="40">
<Border Background="Aqua" />
<TextBlock Text="Hello, WPF!" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Style 在自定義控件的部分并不關(guān)鍵,實(shí)際上你完全可以使用 Button.Template 屬性展開然后實(shí)現(xiàn)同樣的效果,但是缺少?gòu)?fù)用性。
二、猿之手/猴爪難題與依賴屬性
雖然它現(xiàn)在變得比較好看了,但是這個(gè) Button 樣式完全是一個(gè)植物人,成了一個(gè)賽博手辦,好看固然是好看,甚至是可以執(zhí)行 Click 事件來(lái)進(jìn)行交互的,但是我們本來(lái)可以設(shè)置 <Button Background="Red"/> 不能用了。
之所以不能用,原因是因?yàn)槟阍?ControlTemplate 的地方確實(shí)沒有讓 Button 的 Background 發(fā)揮作用。
這種行為就好像下面的代碼一樣:
private void SayHello(string name)
{
Console.WriteLine("hello, world!");
}
請(qǐng)問(wèn),在這個(gè)代碼片段,一個(gè)名為 SayHello 的函數(shù)中,參數(shù) name 的意義是什么?
為此,WPF 引入了依賴屬性 DependencyProperty 的機(jī)制來(lái)讓它就像函數(shù)的參數(shù)意義,能夠?yàn)?ControlTemplate 里面的東西帶來(lái)意義。
在 WPF 的控件中,大部分屬性都屬于依賴屬性,對(duì)于 Button 來(lái)說(shuō),Background 是依賴屬性,Content 也是。
我們是必不可少要學(xué)習(xí)自己創(chuàng)建自定義控件和自定義的依賴屬性的,但是在這一部分,我們來(lái)看一下如何使用自帶的依賴屬性為原生控件進(jìn)行自定義。
<Style x:Key="Button_Test_Style" TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid Width="100" Height="40">
<Border Background="{TemplateBinding Background}" />
<TextBlock Text="Hello, WPF!" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
總而言之,使用 {TemplateBinding XXX} 對(duì)控件的依賴屬性進(jìn)行使用,有的時(shí)候直接使用 TemplateBinding 可能無(wú)法生效,所以你可以使用下面的平替,下面這種的泛用性會(huì)更強(qiáng),但是沒有 TemplateBinding 的寫法那么方便,屬于是比較 Hack 的寫法,我們?cè)诤竺娴慕榻B中有一處只有它才能實(shí)現(xiàn)的效果。
<Style x:Key="Button_Test_Style" TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid Width="100" Height="40">
<Border Background="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Background}" />
<TextBlock Text="Hello, WPF!" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
三、更好看的樣子
當(dāng)你知道了 Background 和 Content 都是依賴屬性之后,我們目前沒有做 TextBlock 呈現(xiàn)內(nèi)容的參數(shù)化模板綁定,但是我想你也應(yīng)該知道怎么做了。
你在進(jìn)行編寫的時(shí)候,可能會(huì)遇到智能提示的問(wèn)題,你會(huì)發(fā)現(xiàn)你在為 TextBlock 的 Text 進(jìn)行綁定的時(shí)候,可能會(huì)發(fā)現(xiàn)你在 VS 的智能提示的小窗里并沒有辦法找到 Content,這并不是 VS 出現(xiàn)了 BUG,VS 的消極反應(yīng)也并不是在否定你,我們打算在美化完 UI 后,再來(lái)細(xì)講為什么 VS 會(huì)如此的不配合,為什么在 TextBlock 的 Text 中綁定 Content 是一個(gè)不算對(duì)也不算錯(cuò)的行為。
<Style x:Key="Button_Test_Style" TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid>
<Border Background="{TemplateBinding Background}" CornerRadius="5" />
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{TemplateBinding Content}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
你可以這樣使用:
<Button
Width="100"
Height="40"
Background="LightGreen"
Content="Hello WPF!"
Style="{StaticResource Button_Test_Style}" />
四、我們的 IDE 到底在抗拒什么,會(huì)不配合我們的 XAML 編寫?
在 VS 的 WPF 編輯環(huán)境中對(duì)于 控件模板 ControlTemplate 和 控件綁定的智能提示來(lái)自于 TargetType="Button" 這邊對(duì)控件的指定,如果沒有指定類型,智能提示會(huì)完全沒有辦法給你補(bǔ)全什么有效代碼。
我們?cè)趯?Background 的時(shí)候很順利但是在 寫 Text 的時(shí)候遇到了 VS 的阻撓,即便如此我們硬寫還是寫出來(lái)了。
效果還不錯(cuò)是吧?
你知道嗎,WPF 的很多控件是支持嵌套的,就像下面這樣:

代碼就像這樣:
<Button Width="100" Height="100">
<TextBlock Text="hello, world!" />
</Button>
你會(huì)發(fā)現(xiàn)你無(wú)法為它再賦予 Content 了,編輯器會(huì)告訴你屬性重復(fù),也就是說(shuō),你在 xaml 里面寫的 Text 屬性為"hello, world!" 的文本塊 TextBlock 控件,已經(jīng)是 Button 的 Content 屬性了。
所以,我想要說(shuō)什么?
Content 這個(gè)依賴屬性能描述的不僅是一個(gè)字符串,它實(shí)際上能描述一個(gè)對(duì)象,它的類型其實(shí)是 object,我們?nèi)タ此亩x就可以知道:
public object Content { get; set; }
正是因?yàn)?Content 是 object 類型,而 Text 屬性接收的要求是 string 字符串,所以 VS 在智能提示的時(shí)候并不認(rèn)為它們倆合適,所以我們?cè)谥悄芴崾镜臅r(shí)候根本找不到它。
那,為什么我們直接寫還是能夠生效?
對(duì)于 Text 這種字符串類型來(lái)說(shuō),我們恰好傳的是 string 字符串這個(gè)對(duì)象,瞎貓碰上死耗子,自然就沒有發(fā)現(xiàn)問(wèn)題。
所以,如果我們的自定義樣式有內(nèi)部嵌套的對(duì)象,它在使用 TemplateBinding 寫法的我們的樣式里,是完全沒有反應(yīng)的。
如果說(shuō)你真的要讓只能接收到 string 的 Text 依賴屬性,被迫吃下那么一坨,不就是 object 嗎,只要是個(gè) object ,使用 ToString() 轉(zhuǎn)成字符串不就好了么。于是,你可以使用上文提到的那種非常冗長(zhǎng)的寫法。
于是就會(huì)有這樣的效果:

<Style x:Key="Button_Test_Style" TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid>
<Border Background="{TemplateBinding Background}" CornerRadius="5" />
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Content}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Button
Width="100"
Height="100"
Background="LightGreen"
Style="{StaticResource Button_Test_Style}">
<TextBlock Name="PART_TextBlock" Text="hello, world!" />
</Button>
五、真正的 Button
為了實(shí)現(xiàn) Button 內(nèi)部控件的嵌套和對(duì)象的嵌套呈現(xiàn),用我們目前的 TextBlock 來(lái)呈現(xiàn)內(nèi)容是完全不可取的。
實(shí)際上一個(gè)標(biāo)準(zhǔn)的 Button 實(shí)現(xiàn)會(huì)使用 ContentPresenter 來(lái)呈現(xiàn),ContentPresenter 本身具備的 Content 依賴屬性才是 Button 的 Content 的依賴屬性最終的去處。
<Style x:Key="Button_Test_Style" TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid>
<Border Background="{TemplateBinding Background}" CornerRadius="5" />
<ContentPresenter
HorizontalAlignment="Center"
VerticalAlignment="Center"
Content="{TemplateBinding Content}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
這個(gè)樣式的效果是這樣的:

代碼:
<Button
Width="100"
Height="100"
Background="LightGreen"
Style="{StaticResource Button_Test_Style}">
<TextBlock Name="PART_TextBlock" Text="hello, world!" />
</Button>
六、Content object 的樣子
1. 若我掏出自定義對(duì)象,你該如何應(yīng)對(duì)?
我們現(xiàn)在能知道的是,對(duì)于字符串類型的 Content 會(huì)顯示一串文字,如果填入的是控件內(nèi)容,它會(huì)顯示控件 UI 的樣子,用來(lái)為按鈕創(chuàng)建圖標(biāo)等相關(guān)需求的時(shí)候會(huì)非常有用。
可是,object 也就意味著是所有類型,我們自己的 class 實(shí)例對(duì)象給它會(huì)是什么樣子?
讓我們?cè)?Button 初始化的時(shí)候在 C# Code-Behind 代碼的部分創(chuàng)建一些內(nèi)容吧!
這是我們自定義的類:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
請(qǐng)注意 xaml 中關(guān)于 Loaded="Button_Loaded" 的部分。
<Button
Width="100"
Height="100"
Background="LightGreen"
Loaded="Button_Loaded"
Style="{StaticResource Button_Test_Style}" />
這是事件訂閱后要執(zhí)行的事情。
private void Button_Loaded(object sender, RoutedEventArgs e)
{
var button = sender as Button;
if (button is null) return;
// 雖然可以寫成 button!.Content 但是怕各位看不懂
button.Content = new Person() { Name = "小明", Age = 18 };
}
這是效果:

因?yàn)槲覀兊捻?xiàng)目叫做 WPFPlayground,所以 Person 這個(gè)對(duì)象被 ToString()后得到的結(jié)果是 WPFPlayground.Person 因?yàn)槌叽缬邢匏阅壳俺尸F(xiàn)的是這樣。
我們希望把信息顯示出來(lái),你可以這樣完善 Person 的內(nèi)容:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public override string ToString()
{
return $"{Name}: {Age} !!!!!";
}
}
效果就會(huì)變成這樣:

2. 若我想要給這個(gè)數(shù)據(jù)自定義外觀,你又該如何應(yīng)對(duì)?
但是有的時(shí)候,我們希望呈現(xiàn)的數(shù)據(jù)也是有布局和 UI 的。
因?yàn)樽远x數(shù)據(jù)的屬性并不是依賴屬性,所以上面在控件模板中介紹的那些方法,TemplateBinding 之類的做法在這邊就完全失效了。
面對(duì)這個(gè)數(shù)據(jù)的外觀定義,你要使用 ContentTemplate 來(lái)做。
當(dāng)然,ContentTemplate 內(nèi)容模板和 ControlTemplate 控件模板長(zhǎng)得很像,但是不一樣的概念,ContentTemplate 和 Content 是一對(duì)內(nèi)容,相互配合才能實(shí)現(xiàn)最好的效果,作為 Content 的好兄弟,ContentTemplate 自然也是依賴屬性,在 ContentPresenter 中發(fā)揮作用。
我們說(shuō)了這么多話,到底想要說(shuō)什么?
我想說(shuō)的是,光寫 Button 中的 ContentTemplate 是沒有用的,因?yàn)閷?shí)際承擔(dān)工作的是 你的 ControlTemplate 控件模板的 ContentPresenter,你需要把這個(gè)依賴屬性作為參數(shù)傳遞進(jìn)去,寫成這個(gè)樣子:
<Style x:Key="Button_Test_Style" TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid>
<Border Background="{TemplateBinding Background}" CornerRadius="5" />
<ContentPresenter
HorizontalAlignment="Center"
VerticalAlignment="Center"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
然后,我們將開始面對(duì) DataTemplate 了。
我們?yōu)?Button 在更新了 Style 后,編寫了對(duì)應(yīng)的 ContentTemplate。
ContentTemplate 的類型是 DataTemplate,集合容器所采用的 ItemTemplate 類型也是 DataTemplate 其實(shí)也是 DataTemplate,因?yàn)槠渲械脑砥鋵?shí)就是每一項(xiàng)套了一個(gè) ContentPresenter 內(nèi)容呈現(xiàn)器。
<Button
Width="100"
Height="100"
Background="LightGreen"
Loaded="Button_Loaded"
Style="{StaticResource Button_Test_Style}">
<Button.ContentTemplate>
<DataTemplate DataType="local:Person">
<StackPanel>
<TextBlock Background="Yellow" Text="{Binding Name}" />
<TextBlock
Background="Aqua"
FontSize="20"
Text="{Binding Age}" />
</StackPanel>
</DataTemplate>
</Button.ContentTemplate>
</Button>
會(huì)變成這樣的效果:

于是,在控件和主題上,你可以為 ControlTemplate 方面打一個(gè)堅(jiān)實(shí)的底子用于項(xiàng)目風(fēng)格的復(fù)用,而在自定義數(shù)據(jù)特別是業(yè)務(wù)數(shù)據(jù)的呈現(xiàn)上,以 ContentTemplate 為代表的 DataTemplate,會(huì)為你帶來(lái)業(yè)務(wù)上的拓展性。
七、所謂集合面板 ItemsControl、ListBox
1. 每一項(xiàng)的外觀樣子使用 ItemTemplate 定義
我們來(lái)創(chuàng)建一個(gè)毫無(wú)通知能力,只是好看的 ViewModel,并不實(shí)現(xiàn) INotifyPropertyChanged。
public class MainViewModel
{
public List<Person> Persons { get; set; }
public MainViewModel()
{
Persons = new List<Person>()
{
new Person(){ Name = "小明", Age = 18},
new Person(){ Name = "小紅", Age = 17},
new Person(){ Name = "小黃", Age = 16},
new Person(){ Name = "小亮", Age = 11},
new Person(){ Name = "小軍", Age = 19},
new Person(){ Name = "小帥", Age = 30},
new Person(){ Name = "小馬", Age = 6},
};
}
}
我相信你知道怎么綁定上下文,所以這邊只做了綁定的部分:
<ItemsControl ItemsSource="{Binding Persons}" />
我們來(lái)看一下效果:

你可以注意到的是,集合容器控件中呈現(xiàn)的樣子,和我們剛才描述的關(guān)于 ContentPresenter 和 Content 的機(jī)制是完全一致的。
讓我們把上面寫的 ContentTemplate 中的 DataTemplate 交給 ItemsControl 的 ItemTemplate 中去。
<ItemsControl ItemsSource="{Binding Persons}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="local:Person">
<StackPanel>
<TextBlock Background="Yellow" Text="{Binding Name}" />
<TextBlock
Background="Aqua"
FontSize="20"
Text="{Binding Age}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
現(xiàn)在的效果就是這樣的:

具體 ItemTemplate 生效的原因來(lái)自于 ContentPresenter,參看源碼:https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/ItemsControl.cs,a32a4ab17d3998f0,references
2. 容器的外觀
你可能會(huì)對(duì) ItemsControl 和 ListBox 的默認(rèn)的 StackPanel 縱向布局感到不滿。
這個(gè)時(shí)候你可以使用 ItemsPanel 創(chuàng)建 ItemsPanelTemplate 對(duì)象,它是屬于 ControlTemplate 相似的,和數(shù)據(jù)無(wú)關(guān)的 Template。
默認(rèn)情況如果寫出來(lái)是這樣的:
<ItemsControl ItemsSource="{Binding Persons}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="local:Person">
<StackPanel>
<TextBlock Background="Yellow" Text="{Binding Name}" />
<TextBlock
Background="Aqua"
FontSize="20"
Text="{Binding Age}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
我如果在使用它,一般我會(huì)使用它的 WrapPanel 和 Horizontal StackPanel。
2.1 WrapPanel 效果
效果:
WrapPanel 的可折疊性。

代碼:
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
2.2
效果:

代碼:
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
八、所謂 DataGrid 和 CellTemplate
這是 DataGrid 的默認(rèn)樣子:
<DataGrid ItemsSource="{Binding Persons}" />
除了顯示一些欄位字段屬性之外,你可能希望能本來(lái)的數(shù)據(jù)源中顯示一些別的控件,比如我們上面定義的 DataTemplate,來(lái)幫助我們更加可視化的看到數(shù)據(jù)。
你可以實(shí)現(xiàn)這樣的效果:

代碼如下:
<DataGrid ItemsSource="{Binding Persons}">
<DataGrid.Columns>
<DataGridTemplateColumn Header="可視化的樣子">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="local:Person">
<StackPanel>
<TextBlock Background="Yellow" Text="{Binding Name}" />
<TextBlock
Background="Aqua"
FontSize="20"
Text="{Binding Age}" />
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
推薦用法:可以用來(lái)顯示重要程度比如紅色綠色、進(jìn)度百分比等等效果,具體想要可視化什么取決于業(yè)務(wù)需求。
九、總結(jié)
| 屬性名 | 類型 | 用法 |
|---|---|---|
| Template | ControlTemplate | 在 Control 控件中的 Template 屬性,是 WPF 最為基礎(chǔ)的內(nèi)容,是所有控件的可復(fù)用性的保障,通常和 Style 搭配,編寫 ControlTemplate 就好比在 xaml 中編寫函數(shù)一樣,具有非常重要的工程意義。 |
| ContentTemplate | DataTemplate | 所有 ContentControl(如 Button)實(shí)現(xiàn),會(huì)在控件模板 ControlTemplate 中的某些 ContentPresenter 將會(huì)承擔(dān)解析和呈現(xiàn)它們的任務(wù),前提是你需要傳遞過(guò)去。 |
| ItemTemplate | DataTemplate | 集合容器之所以能夠呈現(xiàn)內(nèi)容就是因?yàn)樗恳豁?xiàng)都是一個(gè) ContentPresenter,容器將會(huì)把定義的 ItemTemplate 信息交給每一個(gè) ContentPresenter 的 ContentTemplate 中,把每一項(xiàng)的數(shù)據(jù)信息交給 ContentPresenter 的 Content 中,最后實(shí)現(xiàn)列表項(xiàng)的呈現(xiàn),具體會(huì)有 ItemsPresenter 的參與 |
| ItemsPanel | ItemsPanelTemplate | 屬于 ItemsControl 和 ListBox 等數(shù)據(jù)呈呈現(xiàn)容器,用于描述每一項(xiàng)應(yīng)該如何排布,默認(rèn)是 StackPanel Vertical 布局,你完全可以改成 WrapPanel,Canvas,StackPanel Horizontal,Grid 等等的布局容器 |
| CellTemplate | DataTemplate | WPF 出于 DataGrid 更好可視化的角度為你提供的辦法 |

浙公網(wǎng)安備 33010602011771號(hào)