題外話
不出意外,本片內容應該是最后一篇關於.Net技術的博客,做.Net的伙伴們忽噴忽噴。.Net挺好的,微軟最近在跨平台方面搞的水深火熱,更新也比較頻繁,而且博客園的很多大牛也寫的有跨平台相關技術的博客。做.Net開發塊五年時間,個人沒本事,沒做出啥成績。想象偶像梅球王,年齡都差不多,為啥差別就這么大。不甘平庸,想趁機會挑戰下其他方面的技術,正好有一個機會轉前段開發。
對於目前正在從事或者工作中會用到WPF技術開發的伙伴,此片內容不得不收藏,本片介紹的八個問題都是在WPF開發工作中經常使用到並且很容易搞錯的技術點。能輕車熟路的掌握這些問題,那么你的開發效率肯定不會低。
WPF相關鏈接
No.1 准備.Net轉前端開發-WPF界面框架那些事,搭建基礎框架
No.2 准備.Net轉前端開發-WPF界面框架那些事,UI快速實現法
No.3 准備.Net轉前端開發-WPF界面框架那些事,值得珍藏的8個問題
8個問題歸納
No.1.WrapPane和ListBox強強配合;
No.2.給數據綁定轉換器Converter傳多個參數;
No.3.搞清楚路由事件的兩種策略:隧道策略和冒泡策略;
No.4.Conveter比你想象的強大;
No.5.ItemsControl下操作指令的綁定;
No.6.StaticResource和DynamicResource區別;
No.7.數據的幾種綁定形式;
No.8.附加屬性和依賴屬性;
八大經典問題
1. WrapPane和ListBox強強配合
重點:WrapPanel在ListBox面板中的實現方式
看一張需求圖片,圖片中需要實現的功能是:左邊面板顯示內容,右邊是一個圖片列表,由於圖片比較多,所以在左向左拖動中間分隔線時,圖片根據右欄的寬度的增加,每行可顯示多張圖片。功能是很簡單,但有些人寫出這個功能需要2個小時,而有些人只需要十幾分鍾。
循序漸進,我們先實現每行顯示一張圖片,直接使用StackPanel重寫ItemPanel模板,並設置Orientation為Vertical。實現結果如下:
源代碼如下:
<Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="2" /> <ColumnDefinition Width="120" /> </Grid.ColumnDefinitions> <Border BorderBrush="Green" Grid.Column="0"> <Image Source="{Binding ElementName=LBoxImages, Path=SelectedItem.Source}" /> </Border> <GridSplitter Grid.Column="1" VerticalAlignment="Stretch" HorizontalAlignment="Center" BorderThickness="1" BorderBrush="Green" /> <ListBox Name="LBoxImages" Grid.Column="2"> <ListBox.ItemsPanel> <ItemsPanelTemplate> <StackPanel Orientation="Vertical" /> </ItemsPanelTemplate> </ListBox.ItemsPanel> <Image Source="/Images/g1.jpg" Width="100" Height="80" /> <Image Source="/Images/g2.jpg" Width="100" Height="80" /> <Image Source="/Images/g3.jpg" Width="100" Height="80" /> <Image Source="/Images/g4.jpg" Width="100" Height="80" /> <Image Source="/Images/g5.jpg" Width="100" Height="80" /> <Image Source="/Images/g7.jpg" Width="100" Height="80" /> <Image Source="/Images/g8.jpg" Width="100" Height="80" /> </ListBox> </Grid>
分析執行結果圖,游覽顯示的圖片列表,不管ListBox有多寬,總是只顯示一行,現在我們考慮實現每行根據ListBox寬度自動顯示多張圖片。首先,StackPanel是不支持這樣的顯示,而WrapPanel可動態排列每行。所以需要把StackPanel替換為WrapPanel並按行優先排列。修改代碼ItemsPanelTemplate代碼:
<ItemsPanelTemplate> <WrapPanel Orientation="Horizontal" /> </ItemsPanelTemplate>
再看看顯示結果:
和我們的預期效果不一樣,為什么會這樣?正是這個問題讓很多人花幾個小時都沒解決。第一個問題:如果要實現自動換行我們得限制WrapPanel的寬度,所以必須顯示的設置寬度,這個時候很多人都考慮的是把WrapPanel的寬度和ListBox的寬度一致。代碼如下:
<ListBox.ItemsPanel> <ItemsPanelTemplate> <WrapPanel Orientation="Horizontal" Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBox}}, Path=Width}" /> </ItemsPanelTemplate> </ListBox.ItemsPanel>
需要強調的是,這樣修改后結果還是一樣的。為什么會這樣?第二個問題:這里出現的問題和CSS和HTML相似,ListBox有邊框,所以實際顯示內容的Width小於ListBox的Width。而你直接設置WrapPanel的寬度等於ListBox的寬度,肯定會顯示不完,所以滾動條還是存在。因此,我們必須讓WrapPanel的Width小於ListBox的Width。我們可以寫個Conveter,讓WrapPanel的Width等於ListBox的Width減去10個像素。Converter代碼如下:
public class SubConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if(value == null) { throw new ArgumentNullException("value"); } int listBoxWidth; if(!int.TryParse(value.ToString(), out listBoxWidth)) { throw new Exception("invalid value!"); } int subValue = 0; if(parameter != null) { int.TryParse(parameter.ToString(), out subValue); } return listBoxWidth - subValue; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } }
為WrapPanel的Width綁定添加Converter,代碼如下:
<ItemsPanelTemplate> <WrapPanel Orientation="Horizontal" Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBox}}, Path=ActualWidth, Converter={StaticResource SubConverter}, ConverterParameter=10}" /> </ItemsPanelTemplate>
這下就大功告成,看看結果效果,是不是每行自動顯示多張圖片了?
2.給數據綁定轉換器Converter傳多個參數
重點:MultiBinding和IMultiValueConverter。
看看下面功能圖片:
上圖中,有報警、提示、總計三個統計數。總計數是報警和提示數量的總和,如果報警和提示數發生變化,總計數自動更新。其實這個功能也非常簡單,很多人實現的方式是定義一個類,包含三個屬性。代碼如下:
public class Counter : INotifyPropertyChanged { private int _alarmCount; public int AlarmCount { get { return _alarmCount; } set { if(_alarmCount != value) { _alarmCount = value; OnPropertyChanged("AlarmCount"); OnPropertyChanged("TotalCount"); } } } private int _messageCount; public int MessageCount { get { return _messageCount; } set { if(_messageCount != value) { _messageCount = value; OnPropertyChanged("MessageCount"); OnPropertyChanged("TotalCount"); } } } public int TotalCount { get { return AlarmCount + MessageCount; } } }
這里明顯有個問題時,如果統計的數量比較多,那么TotalCount需要加上多個數,並且每個數據屬性都得添加OnPropertyChanged("TotalCount")觸發界面更新TotalCount數據。接下來我們就考慮考慮用Converter去實現該功能,很多人都知道IValueConverter,但有些還沒怎么使用過IMultiValueConverter接口。IMultiValueConverter可接收多個參數。通過IMultiValueConverter,可以讓我們不添加任何后台代碼以及耦合的屬性。IMultiValueConverter實現代碼如下:
public class AdditionConverter : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { if(values == null || values.Length != 2) { return 0; } int alarmCount = 0; if(values[0] != null) { int.TryParse(values[0].ToString(), out alarmCount); } int infoCount = 0; if(values[1] != null) { int.TryParse(values[1].ToString(), out infoCount); } return (alarmCount + infoCount).ToString(); } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { throw new NotImplementedException(); } }
接下來就是通過MultiBinding實現多綁定。代碼如下:
<Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <StackPanel Margin="10" Grid.Column="0" Orientation="Horizontal" Background="Red"> <Label VerticalAlignment="Center" Content="報警:" /> <TextBox Name="TBoxAlarm" Background="Transparent" Height="30" Width="60" VerticalContentAlignment="Center" VerticalAlignment="Center" /> </StackPanel> <StackPanel Margin="10" Grid.Column="1" Orientation="Horizontal" Background="Green"> <Label VerticalAlignment="Center" Content="提示:" /> <TextBox Name="TBoxInfo" Background="Transparent" Height="30" Width="60" VerticalContentAlignment="Center" VerticalAlignment="Center" /> </StackPanel> <StackPanel Margin="10" Grid.Column="2" Orientation="Horizontal" Background="Gray"> <Label VerticalAlignment="Center" Content="總計:" /> <TextBox Name="TBoxTotal" Background="Transparent" Height="30" Width="60" VerticalContentAlignment="Center" VerticalAlignment="Center"> <TextBox.Text> <MultiBinding Converter="{StaticResource AdditionConverter}"> <Binding ElementName="TBoxAlarm" Path="Text" /> <Binding ElementName="TBoxInfo" Path="Text" /> </MultiBinding> </TextBox.Text> </TextBox> </StackPanel> </Grid>
這里,我們沒有修改任何后台代碼以及添加任何Model屬性。這里這是舉個簡單的例子,說明MultiBinding和IMultiValueConverter怎樣使用。
3.搞清楚路由事件的兩種策略:隧道策略和冒泡策略
WPF中元素的事件響由事件路由控制,事件的路由分兩種策略:隧道和冒泡策略。要解釋着兩種策略,太多文字都是廢話,直接上圖分析:
上圖中包含三個Grid,它們的邏輯樹層次關系由上到下依次為Grid1->Grid1_1->Grid1_1_1。例如我們鼠標左鍵單擊Grid1_1_1,隧道策略規定事件的傳遞方向由樹的最上層往下傳遞,在上圖中傳遞的順序為Grid1->Grid1_1->Grid1_1_1。而路由策略規定事件的傳遞方向由邏輯樹的最下層往上傳遞,就像冒泡一樣,從最下面一層一層往上冒泡,傳遞的順序為Grid1_1_1->Grid1_1->Grid1。如果體現在代碼上,隧道策略和冒泡策略有特征?
(1)元素的事件一般都是隧道和冒泡成對存在,隧道事件包含前綴Preiview,而冒泡事件不包含Preview。就拿鼠標左鍵單擊事件舉例,隧道事件為PreviewMouseLeftButtonDown,而冒泡事件為MouseLeftButtonDown。
(1)隧道策略事件先被觸發,隧道策略觸發完后才觸發冒泡策略事件。例如單擊Grid1_1_1,整個路由事件的觸發順序為:Grid1(隧道)->Grid1_1(隧道)->Grid1_1_1(隧道)->Grid1_1_1(冒泡)->Grid1_1(冒泡)->Grid1(冒泡)。
(3)路由事件的EventArgs為RoutedEventArgs,包含Handle屬性。如果在觸發順序的某個事件上設置了Handle等於true,那么它之后的隧道事件和冒泡事件都不會觸發了。
接下來我們就寫例子分析,先看看界面的代碼:
<Grid Width="400" Height="400" Background="Green" Name="Grid1" PreviewMouseLeftButtonDown="Grid1_PreviewMouseLeftButtonDown" MouseLeftButtonDown="Grid1_MouseLeftButtonDown"> <Grid Width="200" Height="200" Background="Red" Name="Grid1_1" PreviewMouseLeftButtonDown="Grid1_1_PreviewMouseLeftButtonDown" MouseLeftButtonDown="Grid1_1_MouseLeftButtonDown"> <Grid Width="100" Height="100" Background="Yellow" Name="Grid1_1_1" PreviewMouseLeftButtonDown="Grid1_1_1_PreviewMouseLeftButtonDown" MouseLeftButtonDown="Grid1_1_1_MouseLeftButtonDown"> </Grid> </Grid> </Grid>
我們為每個Grid都配置了隧道事件PreviewMouseLeftButtonDown,冒泡事件MouseLeftButtonDown。然后分別實現事件內容:
public partial class RoutedEventWindow : Window { public RoutedEventWindow() { InitializeComponent(); } #region 隧道策略 private void Grid1_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { PrintEventLog(sender, e, "隧道"); e.Handled = true; } private void Grid1_1_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { PrintEventLog(sender, e, "隧道"); } private void Grid1_1_1_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { PrintEventLog(sender, e, "隧道"); } #endregion private void PrintEventLog(object sender, RoutedEventArgs e, string action) { var host = sender as FrameworkElement; var source = e.Source as FrameworkElement; var orignalSource = e.OriginalSource as FrameworkElement; Console.WriteLine("策略:{3}, sender: {2}, Source: {0}, OriginalSource: {1}", source.Name, orignalSource.Name, host.Name, action); } #region 冒泡策略 private void Grid1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { PrintEventLog(sender, e, "冒泡策略"); } private void Grid1_1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { PrintEventLog(sender, e, "冒泡策略"); } private void Grid1_1_1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { PrintEventLog(sender, e, "冒泡策略"); } #endregion }
事件的代碼很簡單,都調用了PrintEventLog方法,打印每個事件當前觸發元素(sender)以及觸發源(e.Source)和原始源(e.OriganlSource)。現在我們就把鼠標移到Grid1_1_1上面(黃色),單擊鼠標鼠標左鍵。打印的日志結果為:
策略:隧道, sender: Grid1, Source: Grid1_1_1, OriginalSource: Grid1_1_1
策略:隧道, sender: Grid1_1, Source: Grid1_1_1, OriginalSource: Grid1_1_1
策略:隧道, sender: Grid1_1_1, Source: Grid1_1_1, OriginalSource: Grid1_1_1
策略:冒泡策略, sender: Grid1_1_1, Source: Grid1_1_1, OriginalSource: Grid1_1_1
策略:冒泡策略, sender: Grid1_1, Source: Grid1_1_1, OriginalSource: Grid1_1_1
策略:冒泡策略, sender: Grid1, Source: Grid1_1_1, OriginalSource: Grid1_1_1
分析打印的日志是不是和我們上面描述的特征一樣,先觸發隧道事件(Grid1->Grid1_1->Grid1_1_1),然后再觸發冒泡事件(Grid1_1_1->Grid1_1->Grid1),並且和我們描述的事件傳遞方向也一致?如果還不信我們可以跟進一步測試,我們知道最后一個被觸發的隧道事件是Grid1_1_1上的Grid1_1_1_PreviewMouseLeftButtonDown,如果我在該事件上設置e.Handle等於true,按理來說,后面的3個冒泡事件都不會觸發。代碼如下:
private void Grid1_1_1_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { PrintEventLog(sender, e, "隧道"); e.Handled = true; }
輸出結果如下:
策略:隧道, sender: Grid1, Source: Grid1_1_1, OriginalSource: Grid1_1_1
策略:隧道, sender: Grid1_1, Source: Grid1_1_1, OriginalSource: Grid1_1_1
策略:隧道, sender: Grid1_1_1, Source: Grid1_1_1, OriginalSource: Grid1_1_1
結果和我們之前描的推理一致,后面的3個冒泡事件確實沒有觸發。弄清楚路由事件的機制非常重要,因為在很多時候我們會遇到莫名其妙的問題,例如我們為ListBoxItem添加MouseRightButtonDown事件,但始終沒有被觸發。究其原因,正是因為ListBox自己處理了該事件,設置Handle等於true。那么,ListBox包含的元素的MouseRightButtonDown肯定不會被觸發。
4.Conveter比你想象的強大
什么時候會用到Converter?做WPF的開發人員經常會遇到Enable狀態轉換Visibility狀態、字符串轉Enable狀態、枚舉轉換字符串狀態等。我們一般都知道IValueConverter接口,實現該接口可以很容易的處理上面這些情況。下面是Enable轉Visibility代碼:
public class EnableToVisibilityConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if(value == null || !(value is bool)) { throw new ArgumentNullException("value"); } var result = (bool)value; return result ? Visibility.Visible : Visibility.Collapsed; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } }
另外一種情況,例如我們在列表中顯示某些數據,如果我們想給這些數據加上單位,方法有很多,但用converter實現的應該比較少見。這種情況我們可以充分利用IValueConverter的Convert方法的parameter參數。實現代碼如下:
/// <param name="value">數字</param> /// <param name="targetType"></param> /// <param name="parameter">單位</param> /// <param name="culture"></param> /// <returns></returns> public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if(value == null) { return string.Empty; } if(parameter == null) { return value; } return value.ToString() + " " + parameter.ToString(); }
界面代碼如下:
<TextBlock Text="{Binding Digit, Converter={StaticResource UnitConverter}, ConverterParameter='Kg'}"></TextBlock>
稍微復雜的情況,如果需要根據2個甚至多個值轉換為一個結果。例如我們需要根據一個人的身高、存款、顏值,輸出高富帥、一般、矮窮矬3個狀態。這種情況IValueConverter已不再滿足我們的要求,但Converter給我們提供了IMultiValueConverter接口。該接口可同時接收多個參數。按照前面的需求實現Converter,代碼如下:
public class PersonalStatusConverter : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { //values:身高、顏值、存款 if (values == null || values.Length != 3) { throw new ArgumentException("values"); } int height; //身高 int faceScore; //顏值 decimal savings; //存款 try { if(int.TryParse(values[0].ToString(), out height) && int.TryParse(values[1].ToString(), out faceScore) && decimal.TryParse(values[2].ToString(), out savings)) { //高富帥條件:身高180 CM以上、顏值9分以上、存款1000萬以上 if(height >=180 && faceScore >=9 && savings >= 10000 * 1000) { return "高富帥"; } //矮窮矬條件:身高不高於10CM,顏值小於1,存款不多於1元 if(height <= 10 && faceScore <= 1 && savings <= 1) { return "矮窮矬"; } } } catch (Exception ex){} return "身份未知"; } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { throw new NotImplementedException(); } }
實現了IMultiValueConverter接口后,我們還得知道怎樣使用。這里我們還得配合MultiBinding來實現綁定多個參數。下面就根據一個測試例子看看如何使用它,代碼如下:
<Grid> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="5" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <StackPanel VerticalAlignment="Center" Orientation="Horizontal" Grid.Row="0"> <Label Content="身高(CM):" /> <TextBox Name="TBoxHeight" Width="60"/> <Label Content="顏值(0-10):" /> <TextBox Name="TBoxScore" Width="60" /> <Label Content="存款(元):" /> <TextBox Name="TBoxSaving" Width="100" /> </StackPanel> <StackPanel Grid.Row="2" Orientation="Horizontal" VerticalAlignment="Center"> <Label Content="測試結果:" /> <TextBox IsReadOnly="True" Width="150"> <TextBox.Text> <MultiBinding Converter="{StaticResource PersonalStatusConverter}"> <Binding ElementName="TBoxHeight" Path="Text" /> <Binding ElementName="TBoxScore" Path="Text" /> <Binding ElementName="TBoxSaving" Path="Text" /> </MultiBinding> </TextBox.Text> </TextBox> </StackPanel> </Grid>
通過代碼可以看出MultiBinding和IMultiValueConverter一般都是同時使用的。
測試界面如下:
通過上面的介紹,我們應該知道怎樣使用IValueConverter和IMultiValueConverter接口了。
5.ItemsControl下操作指令的綁定
先看看下面的列表,展示人的頭像、個人信息,並且提供刪除功能。如下所示:
這里最想說的是關於Delete操作的指令綁定,這一點很容易出問題。有些時候我們綁定了指令,但是單擊Delete按鈕沒有任何反應。這里涉及到兩個數據源,一個是列表集合數據源,一個是指令上下文數據源。例如上面的列表存放在UserControl里邊,一般ListItem的數據源來源於List的ItemsSource,而按鈕的Command來源於UserControl數據源。下面是實現的界面代碼:
<Window x:Class="HeaviSoft.Wpf.ErrorDemo.PortraitWindow" 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:HeaviSoft.Wpf.ErrorDemo" xmlns:converter="clr-namespace:HeaviSoft.Wpf.ErrorDemo.Conveters" mc:Ignorable="d" Title="是不是美女" Height="400" Width="600"> <Window.Resources> <converter:SubConverter x:Key="SubConverter"></converter:SubConverter> <Style TargetType="{x:Type Button}"> <Setter Property="Background" Value="Transparent"/> </Style> <Style TargetType="{x:Type TextBlock}"> <Setter Property="FontFamily" Value="KaiTi" /> <Setter Property="Foreground" Value="Honeydew" /> </Style> </Window.Resources> <ListBox ItemsSource="{Binding PortaitList}"> <ListBox.ItemTemplate> <DataTemplate> <Border Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBox}}, Converter={StaticResource SubConverter}, ConverterParameter=10, Path=ActualWidth}" BorderThickness="0, 0, 0, 1" BorderBrush="Gray"> <DockPanel VerticalAlignment="Center" LastChildFill="False"> <Image Margin="5, 0" DockPanel.Dock="Left" Source="{Binding Photo}" Width="40" Height="40" /> <TextBlock DockPanel.Dock="Left" Text="{Binding Name}" VerticalAlignment="Center" /> <TextBlock DockPanel.Dock="Left" Text=", " VerticalAlignment="Center" /> <TextBlock DockPanel.Dock="Left" Text="{Binding Birthday}" VerticalAlignment="Center" /> <TextBlock DockPanel.Dock="Left" Text=", " VerticalAlignment="Center" /> <TextBlock DockPanel.Dock="Left" Text="{Binding Vocation}" VerticalAlignment="Center" /> <Button Margin="0,3, 5, 3" Padding="4, 2" Command="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:PortraitWindow}}, Path=DataContext.DeleteCommand}" CommandParameter="{Binding .}" DockPanel.Dock="Right" Content="Delete" /> </DockPanel> </Border> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Window>
數據源代碼:
public class PortaitViewModel : ModelBase { public PortaitViewModel(IEnumerable<Portait> portaits) { PortaitList = new ObservableCollection<Portait>(portaits); } public ObservableCollection<Portait> PortaitList { get; set; } private ICommand _deleteCommand; public ICommand DeleteCommand { get { return _deleteCommand ?? new UICommand() { Executing = (parameter) => { PortaitList.Remove(parameter as Portait); } }; } } }
首先,PortraitWindow綁定數據源PortaitViewModel,ListBox綁定了一個集合屬性PortaitList,所以ListItem對應集合中的一項,也就是一個Portrait對象。我們重點要看的是Button如何綁定Command。這里再單獨把Button代碼貼出來:
<Button Margin="0,3, 5, 3" Padding="4, 2" Command="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:PortraitWindow}}, Path=DataContext.DeleteCommand}" CommandParameter="{Binding .}" DockPanel.Dock="Right" Content="Delete" />
Button是在ListItem中,我們剛才說ListItem的數據源為一個Portrait對象,所以我們CommandParameter可直接綁定對象{Binding .},而Command需要切換到PortraitWindow的數據上下文中,這里我們使用了RelativeSource,向上遍歷查找邏輯樹PortraitWindow,並綁定它的數據DataContext.DeleteCommand。需要要強調的是x:Type要寫PortraitWindow而不是Window,否則有時候會出問題的。
界面執行結果如下:
6.StaticResource和DynamicResource區別
先不忙描述這兩者的區別,看看應用場景。一般的系統都支持多主題和多語言,我們知道主題和語言都是資源。既然都是資源,那么就涉及到如何引用資源,用StaticResource還是DynamicResource?主題和語言資源的引用必須滿意一個條件,就是我在系統運行過程中,切換主題或語言之后,我們的界面資源馬上也被切換,也就是隨時支持更新。帶着這樣一個場景,再看看兩者的區別:
(1)StaticResource只被加載一次,DynamicResource每次改變都可重新引用。
(2)一般使用DynamicResource的地方都可以使用StaticResource代替。因為DynamicResource只能用着設置依賴屬性的值,而StaticResource可被用到任何地方。例如下面的這種情況就是可使用StaticResource但不能使用DynamicResource:
<Window xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation” xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”> <Window.Resources> <Image x:Key=”zoom” Height=”21” Source=”zoom.gif”/> </Window.Resources> <StackPanel> <StaticResource ResourceKey=”zoom”/> </StackPanel> </Window>
(3)DynamicResource比StaticResource占用更多的開銷,因為它需要額外的跟蹤,跟蹤哪些地方引用了Dynamic資源。如果資源更新了,引用它的地方也會被更新到。
(4)很多人可能覺得DynamicResource加載時間比StaticResource長,但恰恰相反。因為在加載界面時,所有StaticResource引用都會馬上加載出來;而DynamicResource只會在真正使用時才會加載。
7.數據的幾種綁定形式
Binding這個類有時候真把開發人員搞糊塗,特別是它的幾個屬性,像Mode、UpdateSourceTrigger、NotifyOnSourceUpdated、NotifyOnTargetUpdated。所以,想要熟練使用Binding,這幾個屬性不得不掌握。
(1).Mode屬性
是一個枚舉值,枚舉項包括Default、TwoWay(雙向綁定)、OneWay、OneWayToSource、OneTime。Default是一個強大的枚舉項,為什么這么說?如果我們跟蹤代碼,控件的Binding的Mode屬性一般為Default。Default其實就是后面幾個枚舉中的一種情況,具體是哪一種要根據控件的選擇。例如一個TextBox,那么Default為TwoWay模式。如果是一個TextBlock,那么Default為OneWay模式。所以一般情況下,不建議設置Mode。下圖是Mode示意圖,通過圖片可以很清楚的了解這幾個模式:
(2).UpdateSourceTrigger
更新數據源觸發器,分析Mode的示意圖,可看出只有在TwoWay和OneWayToSource情況下才會更新Source。UpdateSourceTrigger也是一個枚舉,枚舉項包括:Default、Explicit、LostFocus、PropertyChanged。和Mode相似,控件默認綁定的UpdateSourceTrigger一般為Default,也是根據不同的情況選擇后面的幾個枚舉值。實際情況,一般觸發器都是為LostFocus,所以有些時候我們需要值發生變化馬上更新數據源,那么必須設置UpdateSourceTrigger=”PropertyChanged”。
(3)NotifyOnSourceUpdated和NotifyOnTargetUpdated
其實這兩個屬性一般都不用配置,NotifyOnSourceUpdated表示當通過控件更新了數據源后是否觸發SourceUpdated事件。NotifyOnTargetUpdated恰好相反,表示當通過數據源更新了控件數據后,是否觸發TargetUpdated事件。既然這兩個屬性控制是否觸發事件,那么這些事件從哪來的?這里要提到Binding的SourceUpdatedEvent和TargetUpdatedEvent事件。下面一段代碼就是連個事件的添加方式:
Binding.AddSourceUpdatedHandler(TBoxResult, new EventHandler<DataTransferEventArgs>((sender1 ,e1) => { /*更新數據源被觸發*/ })); Binding.AddTargetUpdatedHandler(TBoxResult, new EventHandler<DataTransferEventArgs>((sender1, e1) => { /*更新控件數據被觸發*/}));
8.附加屬性和依賴屬性
我們先看看兩者的實現代碼由什么區別,依賴屬性和附加屬性實現代碼如下:
#region 依賴屬性 public static readonly DependencyProperty MyWidthDependencyProperty = DependencyProperty.Register("MyWidth", typeof(int), typeof(CustomControl), new FrameworkPropertyMetadata( (d, e) => { if (d is CustomControl) { } })); public int MyWidth { get { return (int)GetValue(MyWidthDependencyProperty); } set { SetValue(MyWidthDependencyProperty, value); } } #endregion #region 附加屬性 public static readonly DependencyProperty MyHeightDependencyProperty = DependencyProperty.RegisterAttached("MyHeight", typeof(int), typeof(CustomControl), new FrameworkPropertyMetadata( (d, e) => { if (d is CustomControl) { } })); public static void SetMyHeight(DependencyObject obj, int value) { obj.SetValue(MyHeightDependencyProperty, value); } public static int GetMyHeight(DependencyObject obj) { return (int)obj.GetValue(MyHeightDependencyProperty); } #endregion
依賴屬性和附加屬性的聲明方式比較相似,不同的地方一方面依賴屬性調用Register方法,而附加屬性調用RegisterAttached方法。另一方面,依賴屬性一般需要定義一個CLR屬性來使用,而依賴屬性需要定義靜態的Get和Set方法使用。接下來再看看怎樣使用依賴屬性和附加屬性,代碼如下:
<local:CustomControl Grid.Row="1" x:Name="CControl" Width="{Binding ElementName=CControl, Path=MyWidth}" Height="{Binding ElementName=CControl, Path=MyHeight}" MyHeight="{Binding ElementName=TBoxHeight, Path=Text}" local:CustomControl.MyWidth="{Binding ElementName=TBoxWidth, Path=Text}" Background="Green" Margin="10" />
在界面上我們可以直接像使用一般屬性一樣使用依賴屬性:MyHeight=””,而使用附加屬性一般都是類名.附加屬性:local:CustomControl.MyWidth=”“。附加屬性我們見到得也比較多,例如Grid.Row、Grid.RowSpan、DockPanel.Dock等。通過上面的分析,我們總結出兩者的區別:
(1)兩者很相似,都需要定義XXXProperty的靜態只讀屬性;
(2)依賴屬性使用Register方法注冊,而附加屬性使用RegisterAttached注冊;
(3)依賴屬性一般需要定義一個CLR屬性來使用,而附加屬性需要定義Set和Get兩個靜態方法;
(4)依賴屬性定義后,附加的類一直擁有這個依賴屬性。而附加屬性只是需要的時候才附加上去,可有可無;
(5)依賴屬性和附加屬性都可用於擴展類的屬性。但附加屬性可用於界面布局,像Grid的Grid.Row和DockPanel的Dock屬性。
源代碼
完整的代碼存放在GitHub上,代碼路徑:https://github.com/heavis/WpfDemo。
如果本篇內容對大家有幫助,請點擊頁面右下角的關注。如果覺得不好,也歡迎拍磚。你們的評價就是博主的動力!下篇內容,敬請期待!