數據模板
數據模板是一段如何顯示綁定在VM對象的XAML代碼。數據模板可以包含任意元素的組合,基於Binding來顯示不同的信息。
在實際的開發中數據模板的應用場景很多,同樣一個控件可以根據不同的綁定源,根據以設置好的數據模板可以顯示對應的不同的內容。
很多人用不好控件模板和數據模板,覺得有點混亂,大部分都是在追求7天入門WPF,或者直接就問有沒有快速解決我目前問題的辦法,等等。我也沒有,但是數據模板其實比控件模板更好寫,因為他的思路比較簡單,就是這個數據,通過Binding后,界面上要顯示成什么樣。就這樣。然后我們基於這個理解來延伸內容。
首先創建一個用於顯示列表的ListBox我們模仿商店出售的商品。為了便於理解,所有的布局我都使簡單的grid分割不涉及其他布局。
<ListBox Name="ProductsListBox" MaxWidth="290" HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<Border Margin="5" CornerRadius="5" BorderThickness="1" BorderBrush="SteelBlue">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Margin="3" FontWeight="Bold" Text="{Binding Path=Name}"/>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="單價:"/>
<TextBlock Grid.Column="1" Text="{Binding Path=Price}"/>
</Grid>
<Grid Margin="1,0" Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="剩余數量:"/>
<TextBlock Grid.Column="1" Text="{Binding Path=Number}"/>
</Grid>
</Grid>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
我們在ListBox中定義了ItemTemplate的DataTemplate。DataTemplate會再ListBox的ItemsSource綁定數據源后,拿出在DataTemplate中Binding的值,然后按照我們當前得DataTemplate來渲染。
我們定義在ItemTemplate下的數據模板,就是修改ListBox中的子對象如何顯示。
完成代碼如下:
xaml代碼
<Window x:Class="HowtoUseDataTemplate.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:HowtoUseDataTemplate"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="6*"/>
</Grid.ColumnDefinitions>
<ListBox Name="ProductsListBox" MaxWidth="290" HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<Border Margin="5" CornerRadius="5" BorderThickness="1" BorderBrush="SteelBlue">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Margin="3" FontWeight="Bold" Text="{Binding Path=Name}"/>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="單價:"/>
<TextBlock Grid.Column="1" Text="{Binding Path=Price}"/>
</Grid>
<Grid Margin="1,0" Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="剩余數量:"/>
<TextBlock Grid.Column="1" Text="{Binding Path=Number}"/>
</Grid>
</Grid>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Border Margin="3" DataContext="{Binding ElementName=ProductsListBox,Path=SelectedItem}" Grid.Column="1" BorderBrush="AntiqueWhite" BorderThickness="1" CornerRadius="3">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Text="名稱:"/>
<TextBlock Grid.Column="1" Text="{Binding Name}"/>
<TextBlock Grid.Row="1" Text="單價:"/>
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Price}"/>
<TextBlock Grid.Row="2" Text="數量:"/>
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding Number}"/>
<TextBlock Grid.Row="3" Text="介紹:"/>
<TextBlock Grid.Row="3" Grid.Column="1" Text="{Binding Introduce}"/>
</Grid>
</Border>
</Grid>
</Window>
cs代碼如下:
using System.Collections.Generic;
using System.Windows;
namespace HowtoUseDataTemplate
{
/// <summary>
/// MainWindow.xaml 的交互邏輯
/// </summary>
public partial class MainWindow : Window
{
List<Product> products;
public MainWindow()
{
InitializeComponent();
products = new List<Product>()
{
new Product(){Name="火箭彈",Price=1000.00M, Number=5000,Introduce="這玩意要買大量的,便宜又方便、一次齊發。基本上就完成任務了。就是誤差有點大。"},
new Product(){Name="自動步槍(刪除敏感字眼)",Price=300.00M, Number=5000 ,Introduce="單兵裝備"},
new Product(){ Name="爆震彈",Price=50.00M, Number=5000 ,Introduce="單兵裝備"},
new Product(){Name="防彈衣",Price=100.00M, Number=5000,Introduce="單兵裝備"},
new Product(){ Name="防彈頭盔",Price=80.00M, Number=5000,Introduce="單兵裝備"},
new Product(){ Name="特別特別硬之真防彈頭盔",Price=99999.00M, Number=5000,Introduce="單兵裝備名字特別長"},
};
ProductsListBox.ItemsSource = products;
}
}
public class Product
{
public int Number { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Introduce { get; set; }
}
}
數據模板中還有一些常用比較方便的功能。比如數據觸發器。和轉換器,之前講過。在之前講了屬性觸發器、事件觸發器。但是數據觸發器沒講,這里講一下。
(刪除敏感字眼) 舉個例子,火箭彈熱賣了。我們虛擬出來的軟件產品既然包含了這些,產品經理就提出來要添加一個熱賣產品的提醒功能,在產品名稱旁邊添加一個熱賣品的紅色hot斜體的文字顯示,同時文字也要變成紅色。
回想到我們剛才的數據模板,使用數據觸發器來實現熱賣品顯示hot功能。在實體中添加是否是熱賣品屬性。實現轉換器、實現數據觸發器。
1)再工程下添加Converter文件夾
編寫HotBoolToVisibilityConverter類,代碼如下:
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace HowtoUseDataTemplate.Converter
{
public class HotBoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool b)
{
if (b)
{
return Visibility.Visible;
}
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
回到主窗體找到Priduct類,添加IsHot屬性。設置默認值為false。
再Products中設置每個對象是否是熱賣。完整代碼如下:
using System.Collections.Generic;
using System.Windows;
namespace HowtoUseDataTemplate
{
/// <summary>
/// MainWindow.xaml 的交互邏輯
/// </summary>
public partial class MainWindow : Window
{
List<Product> products;
public MainWindow()
{
InitializeComponent();
products = new List<Product>()
{
new Product(){Name="火箭彈",IsHot=true, Price=1000.00M, Number=5000,Introduce="這玩意要買大量的,便宜又方便、一次齊發。基本上就完成任務了。就是誤差有點大。"},
new Product(){Name="自動步槍(刪除敏感字眼)",Price=300.00M, Number=5000 ,Introduce="單兵裝備"},
new Product(){ Name="爆震彈",Price=50.00M, Number=5000 ,Introduce="單兵裝備"},
new Product(){Name="防彈衣",Price=100.00M, Number=5000,Introduce="單兵裝備"},
new Product(){ Name="防彈頭盔",Price=80.00M, Number=5000,Introduce="單兵裝備"},
new Product(){ Name="特別特別硬之真防彈頭盔",Price=99999.00M, Number=5000,Introduce="單兵裝備名字特別長"},
};
ProductsListBox.ItemsSource = products;
}
}
public class Product
{
public int Number { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Introduce { get; set; }
public bool IsHot { get; set; } = false;
}
}
最后回到主窗體的XAML文件下,添加轉換器的引用和資源。
xmlns:converter="clr-namespace:HowtoUseDataTemplate.Converter"
<Window.Resources>
<converter:HotBoolToVisibilityConverter x:Key="HotBoolToVisibilityConverter"/>
</Window.Resources>
找到數據模板中的商品名稱,重新設計Grid布局並添加一個hot文本控件。
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="NameTextBlock" Margin="3" FontWeight="Bold" Text="{Binding Path=Name}"/>
<TextBlock Margin="0,2,2,2" Visibility="{Binding IsHot,Converter={StaticResource HotBoolToVisibilityConverter}}" FontSize="10" Grid.Column="1" FontStyle="Italic" Text="Hot" Foreground="Red"/>
</Grid>
寫一個數據觸發器,當IsHot等於true的時候。去找模板中Name=NameText的控件,設置他的Foreground顏色屬性為紅色,
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=IsHot}" Value="True">
<Setter Property="TextBlock.Foreground" TargetName="NameTextBlock" Value="Red"/>
</DataTrigger>
</DataTemplate.Triggers>
XAML完整代碼如下:
<Window x:Class="HowtoUseDataTemplate.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:converter="clr-namespace:HowtoUseDataTemplate.Converter"
xmlns:local="clr-namespace:HowtoUseDataTemplate"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<converter:HotBoolToVisibilityConverter x:Key="HotBoolToVisibilityConverter"/>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="6*"/>
</Grid.ColumnDefinitions>
<ListBox Name="ProductsListBox" MaxWidth="290" HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=IsHot}" Value="True">
<Setter Property="TextBlock.Foreground" TargetName="NameTextBlock" Value="Red"/>
</DataTrigger>
</DataTemplate.Triggers>
<Border Margin="5" CornerRadius="5" BorderThickness="1" BorderBrush="SteelBlue">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="NameTextBlock" Margin="3" FontWeight="Bold" Text="{Binding Path=Name}"/>
<TextBlock Margin="0,2,2,2" Visibility="{Binding IsHot,Converter={StaticResource HotBoolToVisibilityConverter}}" FontSize="10" Grid.Column="1" FontStyle="Italic" Text="Hot" Foreground="Red"/>
</Grid>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="單價:"/>
<TextBlock Grid.Column="1" Text="{Binding Path=Price}"/>
</Grid>
<Grid Margin="1,0" Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="剩余數量:"/>
<TextBlock Grid.Column="1" Text="{Binding Path=Number}"/>
</Grid>
</Grid>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Border Margin="3" DataContext="{Binding ElementName=ProductsListBox,Path=SelectedItem}" Grid.Column="1" BorderBrush="AntiqueWhite" BorderThickness="1" CornerRadius="3">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Text="名稱:"/>
<TextBlock Grid.Column="1" Text="{Binding Name}"/>
<TextBlock Grid.Row="1" Text="單價:"/>
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Price}"/>
<TextBlock Grid.Row="2" Text="數量:"/>
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding Number}"/>
<TextBlock Grid.Row="3" Text="介紹:"/>
<TextBlock Grid.Row="3" Grid.Column="1" Text="{Binding Introduce}"/>
</Grid>
</Border>
</Grid>
</Window>
效果如下,這樣我們就實現了熱賣的功能。通過數據模板只修改了很少一部分內容。
我們去看上面的XAML代碼,我們現在只有一個簡單的ListBox重寫樣式,就這么多內容需要寫,那如果界面上內容特別多,代碼不是很難讀嗎,我們接下來把代碼拆出去。把DataTemplate寫到當前窗體的Resources中。然后再ListBox中設置ItemTemplate等於我們再資源中定義的數據模板。代碼如下:
<Window x:Class="HowtoUseDataTemplate.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:converter="clr-namespace:HowtoUseDataTemplate.Converter"
xmlns:local="clr-namespace:HowtoUseDataTemplate"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<converter:HotBoolToVisibilityConverter x:Key="HotBoolToVisibilityConverter"/>
<DataTemplate x:Key="ProductItemTemplate">
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=IsHot}" Value="True">
<Setter Property="TextBlock.Foreground" TargetName="NameTextBlock" Value="Red"/>
</DataTrigger>
</DataTemplate.Triggers>
<Border Margin="5" CornerRadius="5" BorderThickness="1" BorderBrush="SteelBlue">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="NameTextBlock" Margin="3" FontWeight="Bold" Text="{Binding Path=Name}"/>
<TextBlock Margin="0,2,2,2" Visibility="{Binding IsHot,Converter={StaticResource HotBoolToVisibilityConverter}}" FontSize="10" Grid.Column="1" FontStyle="Italic" Text="Hot" Foreground="Red"/>
</Grid>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="單價:"/>
<TextBlock Grid.Column="1" Text="{Binding Path=Price}"/>
</Grid>
<Grid Margin="1,0" Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="剩余數量:"/>
<TextBlock Grid.Column="1" Text="{Binding Path=Number}"/>
</Grid>
</Grid>
</Grid>
</Border>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="6*"/>
</Grid.ColumnDefinitions>
<ListBox Name="ProductsListBox" MaxWidth="290" HorizontalContentAlignment="Stretch" ItemTemplate="{StaticResource ProductItemTemplate}">
</ListBox>
<Border Margin="3" DataContext="{Binding ElementName=ProductsListBox,Path=SelectedItem}" Grid.Column="1" BorderBrush="AntiqueWhite" BorderThickness="1" CornerRadius="3">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Text="名稱:"/>
<TextBlock Grid.Column="1" Text="{Binding Name}"/>
<TextBlock Grid.Row="1" Text="單價:"/>
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Price}"/>
<TextBlock Grid.Row="2" Text="數量:"/>
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding Number}"/>
<TextBlock Grid.Row="3" Text="介紹:"/>
<TextBlock Grid.Row="3" Grid.Column="1" Text="{Binding Introduce}"/>
</Grid>
</Border>
</Grid>
</Window>
這樣Grid下的內容就少了很多,結構就清晰了一些。不建議把DataTemplate放到單獨的文件夾中,因為如果App初始化的時候就加載了這些內容的話。可能targetType會影響到所有的使用了這個類的對象。但是其實也有x:key這個可以控制,所以這里具體看需要把。覺得怎么合適怎么來,反正主要的目標都是解耦。
再上面點擊更換Item的時候有個比較討厭的地方,選中的對象有一個藍色的底色改變了,這個是ItemContainerSytle的內容。修改ListBox代碼如下:
<ListBox Name="ProductsListBox" MaxWidth="290" HorizontalContentAlignment="Stretch" ItemTemplate="{StaticResource ProductItemTemplate}">
<ListBox.ItemContainerStyle>
<Style>
<Setter Property="ItemsControl.Padding" Value="0"/>
<Style.Triggers>
<Trigger Property="ListBoxItem.IsSelected" Value="True">
<Setter Property="ListBoxItem.Background" Value=" #84C1FF"/>
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
上面的數據模板中最外層的Border 外面嵌套一層Grid 同時背景色設置為和Listbox背景色一樣,就可以拉。
這篇就寫這么多。到這篇為止就打算基礎部分結束了,下篇講MVVM,IOC。就開始進入財務軟件的項目實戰,因為剩下一些沒講的章節,我整理了一下,大概是Page、Window、控件、TreeView、DataGrid等等。我覺得講控件不屬於入門知識,因為控件實際使用中包含了VM、數據模板、控件模板、Style還有更加深入的列表虛擬化、和數據虛擬化等等。還有怎么調試並分析問題等等。所以這里就不打算講了。以后作為WPF技巧相關的文章,去梳理這些知識點。