题外话

不出意外,本片内容应该是最后一篇关于.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。实现结果如下:

源代码如下:

  1. <Grid>
  2. <Grid.ColumnDefinitions>
  3. <ColumnDefinition Width="*" />
  4. <ColumnDefinition Width="" />
  5. <ColumnDefinition Width="" />
  6. </Grid.ColumnDefinitions>
  7. <Border BorderBrush="Green" Grid.Column="">
  8. <Image Source="{Binding ElementName=LBoxImages, Path=SelectedItem.Source}" />
  9. </Border>
  10. <GridSplitter Grid.Column="" VerticalAlignment="Stretch" HorizontalAlignment="Center" BorderThickness="" BorderBrush="Green" />
  11. <ListBox Name="LBoxImages" Grid.Column="">
  12. <ListBox.ItemsPanel>
  13. <ItemsPanelTemplate>
  14. <StackPanel Orientation="Vertical" />
  15. </ItemsPanelTemplate>
  16. </ListBox.ItemsPanel>
  17. <Image Source="/Images/g1.jpg" Width="" Height="" />
  18. <Image Source="/Images/g2.jpg" Width="" Height="" />
  19. <Image Source="/Images/g3.jpg" Width="" Height="" />
  20. <Image Source="/Images/g4.jpg" Width="" Height="" />
  21. <Image Source="/Images/g5.jpg" Width="" Height="" />
  22. <Image Source="/Images/g7.jpg" Width="" Height="" />
  23. <Image Source="/Images/g8.jpg" Width="" Height="" />
  24. </ListBox>
  25. </Grid>

分析执行结果图,游览显示的图片列表,不管ListBox有多宽,总是只显示一行,现在我们考虑实现每行根据ListBox宽度自动显示多张图片。首先,StackPanel是不支持这样的显示,而WrapPanel可动态排列每行。所以需要把StackPanel替换为WrapPanel并按行优先排列。修改代码ItemsPanelTemplate代码:

  1. <ItemsPanelTemplate>
  2. <WrapPanel Orientation="Horizontal" />
  3. </ItemsPanelTemplate>

再看看显示结果:

和我们的预期效果不一样,为什么会这样?正是这个问题让很多人花几个小时都没解决。第一个问题:如果要实现自动换行我们得限制WrapPanel的宽度,所以必须显示的设置宽度,这个时候很多人都考虑的是把WrapPanel的宽度和ListBox的宽度一致。代码如下:

  1. <ListBox.ItemsPanel>
  2. <ItemsPanelTemplate>
  3. <WrapPanel Orientation="Horizontal" Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBox}}, Path=Width}" />
  4. </ItemsPanelTemplate>
  5. </ListBox.ItemsPanel>

需要强调的是,这样修改后结果还是一样的。为什么会这样?第二个问题:这里出现的问题和CSS和HTML相似,ListBox有边框,所以实际显示内容的Width小于ListBox的Width。而你直接设置WrapPanel的宽度等于ListBox的宽度,肯定会显示不完,所以滚动条还是存在。因此,我们必须让WrapPanel的Width小于ListBox的Width。我们可以写个Conveter,让WrapPanel的Width等于ListBox的Width减去10个像素。Converter代码如下:

  1. public class SubConverter : IValueConverter
  2. {
  3. public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  4. {
  5. if(value == null)
  6. {
  7. throw new ArgumentNullException("value");
  8. }
  9. int listBoxWidth;
  10. if(!int.TryParse(value.ToString(), out listBoxWidth))
  11. {
  12. throw new Exception("invalid value!");
  13. }
  14. int subValue = ;
  15. if(parameter != null)
  16. {
  17. int.TryParse(parameter.ToString(), out subValue);
  18. }
  19. return listBoxWidth - subValue;
  20. }
  21.  
  22. public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  23. {
  24. throw new NotImplementedException();
  25. }
  26. }

为WrapPanel的Width绑定添加Converter,代码如下:

  1. <ItemsPanelTemplate>
  2. <WrapPanel Orientation="Horizontal" Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBox}}, Path=ActualWidth, Converter={StaticResource SubConverter}, ConverterParameter=10}" />
  3. </ItemsPanelTemplate>

这下就大功告成,看看结果效果,是不是每行自动显示多张图片了?

2.给数据绑定转换器Converter传多个参数

重点:MultiBinding和IMultiValueConverter。

看看下面功能图片:

上图中,有报警、提示、总计三个统计数。总计数是报警和提示数量的总和,如果报警和提示数发生变化,总计数自动更新。其实这个功能也非常简单,很多人实现的方式是定义一个类,包含三个属性。代码如下:

  1. public class Counter : INotifyPropertyChanged
  2. {
  3. private int _alarmCount;
  4. public int AlarmCount
  5. {
  6. get { return _alarmCount; }
  7. set
  8. {
  9. if(_alarmCount != value)
  10. {
  11. _alarmCount = value;
  12. OnPropertyChanged("AlarmCount");
  13. OnPropertyChanged("TotalCount");
  14. }
  15. }
  16. }
  17.  
  18. private int _messageCount;
  19. public int MessageCount
  20. {
  21. get { return _messageCount; }
  22. set
  23. {
  24. if(_messageCount != value)
  25. {
  26. _messageCount = value;
  27. OnPropertyChanged("MessageCount");
  28. OnPropertyChanged("TotalCount");
  29. }
  30. }
  31. }
  32.  
  33. public int TotalCount
  34. {
  35. get { return AlarmCount + MessageCount; }
  36. }
  37. }

这里明显有个问题时,如果统计的数量比较多,那么TotalCount需要加上多个数,并且每个数据属性都得添加OnPropertyChanged("TotalCount")触发界面更新TotalCount数据。接下来我们就考虑考虑用Converter去实现该功能,很多人都知道IValueConverter,但有些还没怎么使用过IMultiValueConverter接口。IMultiValueConverter可接收多个参数。通过IMultiValueConverter,可以让我们不添加任何后台代码以及耦合的属性。IMultiValueConverter实现代码如下:

  1. public class AdditionConverter : IMultiValueConverter
  2. {
  3. public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
  4. {
  5. if(values == null || values.Length != )
  6. {
  7. return ;
  8. }
  9. int alarmCount = ;
  10. if(values[] != null)
  11. {
  12. int.TryParse(values[].ToString(), out alarmCount);
  13. }
  14. int infoCount = ;
  15. if(values[] != null)
  16. {
  17. int.TryParse(values[].ToString(), out infoCount);
  18. }
  19.  
  20. return (alarmCount + infoCount).ToString();
  21. }
  22.  
  23. public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
  24. {
  25. throw new NotImplementedException();
  26. }
  27. }

接下来就是通过MultiBinding实现多绑定。代码如下:

  1. <Grid>
  2. <Grid.ColumnDefinitions>
  3. <ColumnDefinition Width="*" />
  4. <ColumnDefinition Width="*" />
  5. <ColumnDefinition Width="*" />
  6. </Grid.ColumnDefinitions>
  7. <StackPanel Margin="" Grid.Column="" Orientation="Horizontal" Background="Red">
  8. <Label VerticalAlignment="Center" Content="报警:" />
  9. <TextBox Name="TBoxAlarm" Background="Transparent" Height="" Width="" VerticalContentAlignment="Center" VerticalAlignment="Center" />
  10. </StackPanel>
  11. <StackPanel Margin="" Grid.Column="" Orientation="Horizontal" Background="Green">
  12. <Label VerticalAlignment="Center" Content="提示:" />
  13. <TextBox Name="TBoxInfo" Background="Transparent" Height="" Width="" VerticalContentAlignment="Center" VerticalAlignment="Center" />
  14. </StackPanel>
  15. <StackPanel Margin="" Grid.Column="" Orientation="Horizontal" Background="Gray">
  16. <Label VerticalAlignment="Center" Content="总计:" />
  17. <TextBox Name="TBoxTotal" Background="Transparent" Height="" Width="" VerticalContentAlignment="Center" VerticalAlignment="Center">
  18. <TextBox.Text>
  19. <MultiBinding Converter="{StaticResource AdditionConverter}">
  20. <Binding ElementName="TBoxAlarm" Path="Text" />
  21. <Binding ElementName="TBoxInfo" Path="Text" />
  22. </MultiBinding>
  23. </TextBox.Text>
  24. </TextBox>
  25. </StackPanel>
  26. </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,那么它之后的隧道事件和冒泡事件都不会触发了。

接下来我们就写例子分析,先看看界面的代码:

  1. <Grid Width="" Height="" Background="Green" Name="Grid1" PreviewMouseLeftButtonDown="Grid1_PreviewMouseLeftButtonDown" MouseLeftButtonDown="Grid1_MouseLeftButtonDown">
  2. <Grid Width="" Height="" Background="Red" Name="Grid1_1" PreviewMouseLeftButtonDown="Grid1_1_PreviewMouseLeftButtonDown" MouseLeftButtonDown="Grid1_1_MouseLeftButtonDown">
  3. <Grid Width="" Height="" Background="Yellow" Name="Grid1_1_1" PreviewMouseLeftButtonDown="Grid1_1_1_PreviewMouseLeftButtonDown" MouseLeftButtonDown="Grid1_1_1_MouseLeftButtonDown">
  4. </Grid>
  5. </Grid>
  6. </Grid>

我们为每个Grid都配置了隧道事件PreviewMouseLeftButtonDown,冒泡事件MouseLeftButtonDown。然后分别实现事件内容:

  1. public partial class RoutedEventWindow : Window
  2. {
  3. public RoutedEventWindow()
  4. {
  5. InitializeComponent();
  6. }
  7. #region 隧道策略
  8. private void Grid1_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
  9. {
  10. PrintEventLog(sender, e, "隧道");
  11. e.Handled = true;
  12. }
  13.  
  14. private void Grid1_1_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
  15. {
  16. PrintEventLog(sender, e, "隧道");
  17. }
  18.  
  19. private void Grid1_1_1_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
  20. {
  21. PrintEventLog(sender, e, "隧道");
  22. }
  23.  
  24. #endregion
  25.  
  26. private void PrintEventLog(object sender, RoutedEventArgs e, string action)
  27. {
  28. var host = sender as FrameworkElement;
  29. var source = e.Source as FrameworkElement;
  30. var orignalSource = e.OriginalSource as FrameworkElement;
  31. Console.WriteLine("策略:{3}, sender: {2}, Source: {0}, OriginalSource: {1}", source.Name, orignalSource.Name, host.Name, action);
  32. }
  33.  
  34. #region 冒泡策略
  35.  
  36. private void Grid1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
  37. {
  38. PrintEventLog(sender, e, "冒泡策略");
  39. }
  40.  
  41. private void Grid1_1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
  42. {
  43. PrintEventLog(sender, e, "冒泡策略");
  44. }
  45.  
  46. private void Grid1_1_1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
  47. {
  48. PrintEventLog(sender, e, "冒泡策略");
  49. }
  50.  
  51. #endregion
  52. }

事件的代码很简单,都调用了PrintEventLog方法,打印每个事件当前触发元素(sender)以及触发源(e.Source)和原始源(e.OriganlSource)。现在我们就把鼠标移到Grid1_1_1上面(黄色),单击鼠标鼠标左键。打印的日志结果为:

  1. 策略:隧道, sender: Grid1, Source: Grid1_1_1, OriginalSource: Grid1_1_1
  2. 策略:隧道, sender: Grid1_1, Source: Grid1_1_1, OriginalSource: Grid1_1_1
  3. 策略:隧道, sender: Grid1_1_1, Source: Grid1_1_1, OriginalSource: Grid1_1_1
  4. 策略:冒泡策略, sender: Grid1_1_1, Source: Grid1_1_1, OriginalSource: Grid1_1_1
  5. 策略:冒泡策略, sender: Grid1_1, Source: Grid1_1_1, OriginalSource: Grid1_1_1
  6. 策略:冒泡策略, 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个冒泡事件都不会触发。代码如下:

  1. private void Grid1_1_1_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
  2. {
  3. PrintEventLog(sender, e, "隧道");
  4. e.Handled = true;
  5. }

输出结果如下:

  1. 策略:隧道, sender: Grid1, Source: Grid1_1_1, OriginalSource: Grid1_1_1
  2. 策略:隧道, sender: Grid1_1, Source: Grid1_1_1, OriginalSource: Grid1_1_1
  3. 策略:隧道, 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代码:

  1. public class EnableToVisibilityConverter : IValueConverter
  2. {
  3. public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  4. {
  5. if(value == null || !(value is bool))
  6. {
  7. throw new ArgumentNullException("value");
  8. }
  9. var result = (bool)value;
  10.  
  11. return result ? Visibility.Visible : Visibility.Collapsed;
  12. }
  13.  
  14. public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  15. {
  16. throw new NotImplementedException();
  17. }
  18. }

另外一种情况,例如我们在列表中显示某些数据,如果我们想给这些数据加上单位,方法有很多,但用converter实现的应该比较少见。这种情况我们可以充分利用IValueConverter的Convert方法的parameter参数。实现代码如下:

  1. /// <param name="value">数字</param>
  2. /// <param name="targetType"></param>
  3. /// <param name="parameter">单位</param>
  4. /// <param name="culture"></param>
  5. /// <returns></returns>
  6. public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  7. {
  8. if(value == null)
  9. {
  10. return string.Empty;
  11. }
  12. if(parameter == null)
  13. {
  14. return value;
  15. }
  16. return value.ToString() + " " + parameter.ToString();
  17. }

界面代码如下:

  1. <TextBlock Text="{Binding Digit, Converter={StaticResource UnitConverter}, ConverterParameter='Kg'}"></TextBlock>

稍微复杂的情况,如果需要根据2个甚至多个值转换为一个结果。例如我们需要根据一个人的身高、存款、颜值,输出高富帅、一般、矮穷矬3个状态。这种情况IValueConverter已不再满足我们的要求,但Converter给我们提供了IMultiValueConverter接口。该接口可同时接收多个参数。按照前面的需求实现Converter,代码如下:

  1. public class PersonalStatusConverter : IMultiValueConverter
  2. {
  3. public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
  4. {
  5. //values:身高、颜值、存款
  6. if (values == null || values.Length != )
  7. {
  8. throw new ArgumentException("values");
  9. }
  10. int height; //身高
  11. int faceScore; //颜值
  12. decimal savings; //存款
  13.  
  14. try
  15. {
  16. if(int.TryParse(values[].ToString(), out height) &&
  17. int.TryParse(values[].ToString(), out faceScore) &&
  18. decimal.TryParse(values[].ToString(), out savings))
  19. {
  20. //高富帅条件:身高180 CM以上、颜值9分以上、存款1000万以上
  21. if(height >= && faceScore >= && savings >= * )
  22. {
  23. return "高富帅";
  24. }
  25. //矮穷矬条件:身高不高于10CM,颜值小于1,存款不多于1元
  26. if(height <= && faceScore <= && savings <= )
  27. {
  28. return "矮穷矬";
  29. }
  30. }
  31. }
  32. catch (Exception ex){}
  33.  
  34. return "身份未知";
  35. }
  36.  
  37. public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
  38. {
  39. throw new NotImplementedException();
  40. }
  41. }

实现了IMultiValueConverter接口后,我们还得知道怎样使用。这里我们还得配合MultiBinding来实现绑定多个参数。下面就根据一个测试例子看看如何使用它,代码如下:

  1. <Grid>
  2. <Grid.RowDefinitions>
  3. <RowDefinition Height="*" />
  4. <RowDefinition Height="" />
  5. <RowDefinition Height="*" />
  6. </Grid.RowDefinitions>
  7. <StackPanel VerticalAlignment="Center" Orientation="Horizontal" Grid.Row="">
  8. <Label Content="身高(CM):" />
  9. <TextBox Name="TBoxHeight" Width=""/>
  10. <Label Content="颜值(0-10):" />
  11. <TextBox Name="TBoxScore" Width="" />
  12. <Label Content="存款(元):" />
  13. <TextBox Name="TBoxSaving" Width="" />
  14. </StackPanel>
  15. <StackPanel Grid.Row="" Orientation="Horizontal" VerticalAlignment="Center">
  16. <Label Content="测试结果:" />
  17. <TextBox IsReadOnly="True" Width="">
  18. <TextBox.Text>
  19. <MultiBinding Converter="{StaticResource PersonalStatusConverter}">
  20. <Binding ElementName="TBoxHeight" Path="Text" />
  21. <Binding ElementName="TBoxScore" Path="Text" />
  22. <Binding ElementName="TBoxSaving" Path="Text" />
  23. </MultiBinding>
  24. </TextBox.Text>
  25. </TextBox>
  26. </StackPanel>
  27. </Grid>

通过代码可以看出MultiBinding和IMultiValueConverter一般都是同时使用的。

测试界面如下:

通过上面的介绍,我们应该知道怎样使用IValueConverter和IMultiValueConverter接口了。

5.ItemsControl下操作指令的绑定

先看看下面的列表,展示人的头像、个人信息,并且提供删除功能。如下所示:

这里最想说的是关于Delete操作的指令绑定,这一点很容易出问题。有些时候我们绑定了指令,但是单击Delete按钮没有任何反应。这里涉及到两个数据源,一个是列表集合数据源,一个是指令上下文数据源。例如上面的列表存放在UserControl里边,一般ListItem的数据源来源于List的ItemsSource,而按钮的Command来源于UserControl数据源。下面是实现的界面代码:

  1. <Window x:Class="HeaviSoft.Wpf.ErrorDemo.PortraitWindow"
  2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4. xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  5. xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  6. xmlns:local="clr-namespace:HeaviSoft.Wpf.ErrorDemo"
  7. xmlns:converter="clr-namespace:HeaviSoft.Wpf.ErrorDemo.Conveters"
  8. mc:Ignorable="d"
  9. Title="是不是美女" Height="" Width="">
  10. <Window.Resources>
  11. <converter:SubConverter x:Key="SubConverter"></converter:SubConverter>
  12. <Style TargetType="{x:Type Button}">
  13. <Setter Property="Background" Value="Transparent"/>
  14. </Style>
  15. <Style TargetType="{x:Type TextBlock}">
  16. <Setter Property="FontFamily" Value="KaiTi" />
  17. <Setter Property="Foreground" Value="Honeydew" />
  18. </Style>
  19. </Window.Resources>
  20. <ListBox ItemsSource="{Binding PortaitList}">
  21. <ListBox.ItemTemplate>
  22. <DataTemplate>
  23. <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">
  24. <DockPanel VerticalAlignment="Center" LastChildFill="False">
  25. <Image Margin="5, 0" DockPanel.Dock="Left" Source="{Binding Photo}" Width="" Height="" />
  26. <TextBlock DockPanel.Dock="Left" Text="{Binding Name}" VerticalAlignment="Center" />
  27. <TextBlock DockPanel.Dock="Left" Text=", " VerticalAlignment="Center" />
  28. <TextBlock DockPanel.Dock="Left" Text="{Binding Birthday}" VerticalAlignment="Center" />
  29. <TextBlock DockPanel.Dock="Left" Text=", " VerticalAlignment="Center" />
  30. <TextBlock DockPanel.Dock="Left" Text="{Binding Vocation}" VerticalAlignment="Center" />
  31. <Button Margin="0,3, 5, 3" Padding="4, 2"
  32. Command="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:PortraitWindow}}, Path=DataContext.DeleteCommand}" CommandParameter="{Binding .}" DockPanel.Dock="Right" Content="Delete" />
  33. </DockPanel>
  34. </Border>
  35. </DataTemplate>
  36. </ListBox.ItemTemplate>
  37. </ListBox>
  38. </Window>

数据源代码:

  1. public class PortaitViewModel : ModelBase
  2. {
  3. public PortaitViewModel(IEnumerable<Portait> portaits)
  4. {
  5. PortaitList = new ObservableCollection<Portait>(portaits);
  6. }
  7.  
  8. public ObservableCollection<Portait> PortaitList { get; set; }
  9.  
  10. private ICommand _deleteCommand;
  11. public ICommand DeleteCommand
  12. {
  13. get
  14. {
  15. return _deleteCommand ?? new UICommand()
  16. {
  17. Executing = (parameter) =>
  18. {
  19. PortaitList.Remove(parameter as Portait);
  20. }
  21. };
  22. }
  23. }
  24. }

首先,PortraitWindow绑定数据源PortaitViewModel,ListBox绑定了一个集合属性PortaitList,所以ListItem对应集合中的一项,也就是一个Portrait对象。我们重点要看的是Button如何绑定Command。这里再单独把Button代码贴出来:

  1. <Button Margin="0,3, 5, 3" Padding="4, 2"
  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:

  1. <Window xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
  2. xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”>
  3. <Window.Resources>
  4. <Image x:Key=”zoom” Height=”” Source=”zoom.gif”/>
  5. </Window.Resources>
  6. <StackPanel>
  7. <StaticResource ResourceKey=”zoom”/>
  8. </StackPanel>
  9. </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事件。下面一段代码就是连个事件的添加方式:

  1. Binding.AddSourceUpdatedHandler(TBoxResult, new EventHandler<DataTransferEventArgs>((sender1 ,e1) => { /*更新数据源被触发*/ }));
  2. Binding.AddTargetUpdatedHandler(TBoxResult, new EventHandler<DataTransferEventArgs>((sender1, e1) => { /*更新控件数据被触发*/}));

8.附加属性和依赖属性

我们先看看两者的实现代码由什么区别,依赖属性和附加属性实现代码如下:

  1. #region 依赖属性
  2. public static readonly DependencyProperty MyWidthDependencyProperty = DependencyProperty.Register("MyWidth", typeof(int), typeof(CustomControl), new FrameworkPropertyMetadata(
  3. (d, e) =>
  4. {
  5. if (d is CustomControl)
  6. {
  7. }
  8. }));
  9.  
  10. public int MyWidth
  11. {
  12. get { return (int)GetValue(MyWidthDependencyProperty); }
  13. set { SetValue(MyWidthDependencyProperty, value); }
  14. }
  15.  
  16. #endregion
  17.  
  18. #region 附加属性
  19. public static readonly DependencyProperty MyHeightDependencyProperty = DependencyProperty.RegisterAttached("MyHeight", typeof(int), typeof(CustomControl), new FrameworkPropertyMetadata(
  20. (d, e) =>
  21. {
  22. if (d is CustomControl)
  23. {
  24. }
  25. }));
  26.  
  27. public static void SetMyHeight(DependencyObject obj, int value)
  28. {
  29. obj.SetValue(MyHeightDependencyProperty, value);
  30. }
  31.  
  32. public static int GetMyHeight(DependencyObject obj)
  33. {
  34. return (int)obj.GetValue(MyHeightDependencyProperty);
  35. }
  36. #endregion

依赖属性和附加属性的声明方式比较相似,不同的地方一方面依赖属性调用Register方法,而附加属性调用RegisterAttached方法。另一方面,依赖属性一般需要定义一个CLR属性来使用,而依赖属性需要定义静态的Get和Set方法使用。接下来再看看怎样使用依赖属性和附加属性,代码如下:

  1. <local:CustomControl Grid.Row="" x:Name="CControl"
  2. Width="{Binding ElementName=CControl, Path=MyWidth}"
  3. Height="{Binding ElementName=CControl, Path=MyHeight}"
  4. MyHeight="{Binding ElementName=TBoxHeight, Path=Text}"
  5. local:CustomControl.MyWidth="{Binding ElementName=TBoxWidth, Path=Text}"
  6. Background="Green" Margin="" />

在界面上我们可以直接像使用一般属性一样使用依赖属性: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

如果本篇内容对大家有帮助,请点击页面右下角的关注。如果觉得不好,也欢迎拍砖。你们的评价就是博主的动力!下篇内容,敬请期待!

准备.Net转前端开发-WPF界面框架那些事,值得珍藏的8个问题的更多相关文章

  1. 准备.Net转前端开发-WPF界面框架那些事,搭建基础框架

    题外话 最近都没怎么写博客,主要是最近在看WPF方面的书<wpf-4-unleashed.pdf>,挑了比较重要的几个章节学习了下WPF基础技术.另外,也把这本书推荐给目前正在从事WPF开 ...

  2. 准备.Net转前端开发-WPF界面框架那些事,UI快速实现法

    题外话 打开博客园,查看首页左栏的”推荐博客”,排名前五的博客分别是(此处非广告):Artech.小坦克.圣殿骑士.腾飞(Jesse).数据之巅.再看看它们博客的最新更新时间:Artech(2014- ...

  3. 分享非常漂亮的WPF界面框架源码及插件化实现原理

      在上文<分享一个非常漂亮的WPF界面框架>中我简单的介绍了一个界面框架,有朋友已经指出了,这个界面框架是基于ModernUI来实现的,在该文我将分享所有的源码,并详细描述如何基于Mod ...

  4. 分享一个漂亮WPF界面框架创作过程及其源码

    本文会作为一个系列,分为以下部分来介绍: (1)见识一下这个界面框架: (2)界面框架如何进行开发: (3)辅助开发支持:Demo.模板.VsPackage制作. 框架源码如下所示. 本文介绍第(1) ...

  5. 分享一个漂亮WPF界面框架创作过程及其源码(转)

    本文会作为一个系列,分为以下部分来介绍: (1)见识一下这个界面框架: (2)界面框架如何进行开发: (3)辅助开发支持:Demo.模板.VsPackage制作. 框架源码如下所示. 本文介绍第(1) ...

  6. 关于WPF界面框架MahApps.Metro的一个BUG

    碰到了这个问题,记录一下,以便以后查阅: 在一个WPF项目中使用MahApps.Metro界面框架,其中有一个功能是嵌入一个带句柄的标记. 首先WPF是出了窗体和WebBrowser带有句柄外,其他控 ...

  7. openresty 前端开发轻量级MVC框架封装一(控制器篇)

    通过前面几章,我们已经掌握了一些基本的开发知识,但是代码结构比较简单,缺乏统一的标准,模块化,也缺乏统一的异常处理,这一章我们主要来学习如何封装一个轻量级的MVC框架,规范以及简化开发,并且提供类似p ...

  8. openresty 前端开发轻量级MVC框架封装二(渲染篇)

    这一章主要介绍怎么使用模板,进行后端渲染,主要用到了lua-resty-template这个库,直接下载下来,放到lualib里面就行了,推荐第三方库,已经框架都放到lualib目录里面,lua目录放 ...

  9. WPF界面框架的设计

    http://www.cnblogs.com/baihmpgy/p/osgi_muinavtree_fx.html

随机推荐

  1. Logstash——multiline 插件,匹配多行日志

    本文内容 测试数据 字段属性 按多行解析运行时日志 把多行日志解析到字段 参考资料 在处理日志时,除了访问日志外,还要处理运行时日志,该日志大都用程序写的,比如 log4j.运行时日志跟访问日志最大的 ...

  2. Ubuntu上安装和使用SSH,Xming+PuTTY在Windows下远程Linux主机使用图形界面的程序

    自:http://blog.csdn.net/neofung/article/details/6574002 Ubuntu上安装和使用SSH  网上有很多介绍在Ubuntu下开启SSH服务的文章,但大 ...

  3. 简明易懂的call apply

    在iteye看到一篇对call解释得相当简明易懂,觉得得宣传一下 : http://uule.iteye.com/blog/1158829 一.方法的定义 call方法: 语法:call([thisO ...

  4. 利用 a 标签自动解析 url

    很多时候,我们有从 url 中提取域名,查询关键字,变量参数值等的需求,然而我们可以让浏览器方便地帮助我们完成这一任务而不用写正则去抓取.方法就是先创建一个 a 标签然后将需要解析的 url 赋值给  ...

  5. 由于源码使用是c\c++与oc混编导致Unknown type name 'NSString'

    今天看到个问题,编辑工程提示Unknown type name 'NSString',如下图 解决方案三: 将Compile Sources As 改为 Objective-C++

  6. 查看SqlAzure和SQLServer中的每个表数据行数

    SqlAzure中的方式: select t.name ,s.row_count from sys.tables t join sys.dm_db_partition_stats s ON t.obj ...

  7. iOS 自定义滑动切换TabBar

    貌似经常会用到,自己整理收藏起来,方便日后查找备用. 效果如图: 由于制作gif,调整了属性,所以看起来的效果不好.如果用默认配置,生成的gif会很大. 制作gif: 1.使用QuickTimePla ...

  8. English Metric Units and Open XML

    English Metric Units and Open XML 在Open XML里使用了English Metric Units(EMUs)来作为度量单位.比如 public class Ext ...

  9. Mac OS X Tips

    命令行查看Mac OS X版本 $ sw_vers ProductName: Mac OS X ProductVersion: BuildVersion: 14D131 Mac OS X截图 不要使用 ...

  10. sql 字符次数

    FParentPath 查询字段 本条语句 条件是 ,  查询 , 在这个字段出现了几次 1=没有 2=1次 3=2次(依次累加)