WPF自定义依赖集合属性无法触发更新的问题

  通常WPF中通过继承UserControl的来快速创建自定义控件,最近项目上需要设计一个卫星星图显示控件,最终效果如下图所示。完成过程中遇到了自定义集合依赖属性无法触发更新通知的问题,在此记录一下,方便有相同问题的朋友们可以快速解决,也希望有人能发现更好的解决办法。

  为了完成目的,我写了下面一个SateChart自定义控件类,

XAML代码如下:

<UserControl x:Class="WpfApplication1.SateChart"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300" >
    <DockPanel Margin="5" LastChildFill="True">
        <StackPanel Margin="1" DockPanel.Dock="Top" Orientation="Horizontal" HorizontalAlignment="Center">
            <Rectangle Fill="Orange" Width="10" Height="10"/>
            <CheckBox Name="bdView" Content="BD-2" Margin="5,0,5,0"  IsChecked="True" Click="bdView_Click"/>
            <Rectangle Fill="Red" Width="10" Height="10"/>
            <CheckBox Name="gpsView" Content="GPS" Margin="5,0,5,0"    IsChecked="True" Click="gpsView_Click"/>
            <Rectangle Fill="Blue" Width="10" Height="10"/>
            <CheckBox Name="glnssView" Content="GLNS" Margin="5,0,5,0"   IsChecked="True" Click="glnssView_Click"/>
        </StackPanel>
        <Viewbox DockPanel.Dock="Bottom" MaxHeight="300" MaxWidth="300">
            <Canvas Name="myCanvas" Width="90" Height="90" >
                <Ellipse Name="e1" Width="90" Height="90" Fill="Black" Stroke="Black"
                                 HorizontalAlignment="Center"/>
                <Ellipse Name="e2" Width="90" Height="90" Fill="Black" Stroke="White"
                                 HorizontalAlignment="Center">
                    <Ellipse.RenderTransform>
                        <ScaleTransform ScaleX="0.8333" ScaleY="0.8333" CenterX="45" CenterY="45"/>
                    </Ellipse.RenderTransform>
                </Ellipse>
                <Ellipse Name="e3" Width="90" Height="90" Fill="Black" Stroke="White"
                                 HorizontalAlignment="Center">
                    <Ellipse.RenderTransform>
                        <ScaleTransform ScaleX="0.6666" ScaleY="0.6666" CenterX="45" CenterY="45"/>
                    </Ellipse.RenderTransform>
                </Ellipse>
                <Ellipse Name="e4" Width="90" Height="90" Fill="Black" Stroke="White"
                                 HorizontalAlignment="Center">
                    <Ellipse.RenderTransform>
                        <ScaleTransform ScaleX="0.5" ScaleY="0.5" CenterX="45" CenterY="45"/>
                    </Ellipse.RenderTransform>
                </Ellipse>
                <Ellipse Name="e5" Width="90" Height="90" Fill="Black" Stroke="White"
                                 HorizontalAlignment="Center">
                    <Ellipse.RenderTransform>
                        <ScaleTransform ScaleX="0.333" ScaleY="0.333" CenterX="45" CenterY="45"/>
                    </Ellipse.RenderTransform>
                </Ellipse>
                <Ellipse Name="e6" Width="90" Height="90" Fill="Black" Stroke="White"
                                 HorizontalAlignment="Center">
                    <Ellipse.RenderTransform>
                        <ScaleTransform ScaleX="0.1666" ScaleY="0.1666" CenterX="45" CenterY="45"/>
                    </Ellipse.RenderTransform>
                </Ellipse>
                <Line Name="line0"  Stroke="White" StrokeDashArray="1 2" StrokeThickness="0.3" X1="45" Y1="90" X2="45" Y2="0" />
                <Line Name="line1"  Stroke="White" StrokeDashArray="1 2" StrokeThickness="0.3" X1="45" Y1="90" X2="45" Y2="0" >
                    <Line.RenderTransform>
                        <RotateTransform Angle="30" CenterX="45" CenterY="45"/>
                    </Line.RenderTransform>
                </Line>
                <Line Name="line2"  Stroke="White" StrokeDashArray="1 2" StrokeThickness="0.3" X1="45" Y1="90" X2="45" Y2="0" >
                    <Line.RenderTransform>
                        <RotateTransform Angle="60" CenterX="45" CenterY="45"/>
                    </Line.RenderTransform>
                </Line>
                <Line Name="line3"  Stroke="White" StrokeDashArray="1 2" StrokeThickness="0.3" X1="45" Y1="90" X2="45" Y2="0"  >
                    <Line.RenderTransform>
                        <RotateTransform Angle="90" CenterX="45" CenterY="45"/>
                    </Line.RenderTransform>
                </Line>
                <Line Name="line4"  Stroke="White" StrokeDashArray="1 2" StrokeThickness="0.3" X1="45" Y1="90" X2="45" Y2="0" >
                    <Line.RenderTransform>
                        <RotateTransform Angle="120" CenterX="45" CenterY="45"/>
                    </Line.RenderTransform>
                </Line>
                <Line Name="line5"  Stroke="White" StrokeDashArray="1 2" StrokeThickness="0.3" X1="45" Y1="90" X2="45" Y2="0" >
                    <Line.RenderTransform>
                        <RotateTransform Angle="150" CenterX="45" CenterY="45"/>
                    </Line.RenderTransform>
                </Line>
                <TextBlock Name="tb1" Text="360°" Canvas.Left="43"  Canvas.Top="0" Foreground="White" FontSize="2"/>
                <TextBlock Name="tb2" Text="30°" Canvas.Left="66" Canvas.Top="7" Foreground="White" FontSize="2" />
                <TextBlock Name="tb3" Text="60°" Canvas.Left="80" Canvas.Top="22" Foreground="White" FontSize="2" />
                <TextBlock Name="tb4" Text="90°" Canvas.Left="86" Canvas.Top="45" Foreground="White" FontSize="2" />
                <TextBlock Name="tb5" Text="120°" Canvas.Left="80" Canvas.Top="64" Foreground="White" FontSize="2" />
                <TextBlock Name="tb6" Text="150°" Canvas.Left="65" Canvas.Top="80" Foreground="White" FontSize="2" />
                <TextBlock Name="tb7" Text="180°" Canvas.Left="43" Canvas.Top="86" Foreground="White" FontSize="2" />
                <TextBlock Name="tb8" Text="210°" Canvas.Left="25" Canvas.Top="82" Foreground="White" FontSize="2" />
                <TextBlock Name="tb9" Text="240°" Canvas.Left="8" Canvas.Top="67" Foreground="White" FontSize="2" />
                <TextBlock Name="tb10" Text="270°" Canvas.Left="1" Canvas.Top="45" Foreground="White" FontSize="2" />
                <TextBlock Name="tb11" Text="300°" Canvas.Left="8" Canvas.Top="21" Foreground="White" FontSize="2" />
                <TextBlock Name="tb12" Text="330°" Canvas.Left="25" Canvas.Top="6" Foreground="White" FontSize="2" />
                <TextBlock Name="tb13" Text="15°" Canvas.Left="52" Canvas.Top="6" Foreground="White" FontSize="2" />
                <TextBlock Name="tb14" Text="30°" Canvas.Left="50" Canvas.Top="14" Foreground="White" FontSize="2" />
                <TextBlock Name="tb15" Text="45°" Canvas.Left="48" Canvas.Top="21" Foreground="White" FontSize="2" />
                <TextBlock Name="tb16" Text="60°" Canvas.Left="47" Canvas.Top="29" Foreground="White" FontSize="2" />
                <TextBlock Name="tb17" Text="75°" Canvas.Left="46" Canvas.Top="35" Foreground="White" FontSize="2" />
            </Canvas>
        </Viewbox>
    </DockPanel>
</UserControl>
View Code

后台代码如下:

  1 public partial class SateChart : UserControl
  2     {
  3         static bool isShowBD = true;
  4         static bool isShowGPS = true;
  5         static bool isShowGLONASS = true;
  6         //卫星图标半径
  7         static int r = 45;
  8         public static DependencyProperty SateSourceProperty;
  9 
 10         public IEnumerable<Sate> SateSource
 11         {
 12             get { return (IEnumerable<Sate>)GetValue(SateSourceProperty); }
 13             set { SetValue(SateSourceProperty, value); }
 14         }
 15         static SateChart()
 16         {
 17             SateSourceProperty = DependencyProperty.Register(
 18                 "SateSource", typeof(IEnumerable<Sate>), typeof(SateChart), new FrameworkPropertyMetadata(
 19                     null, new PropertyChangedCallback(OnSateSourceChanged)));
 20         }
 21         public SateChart()
 22         {
 23             InitializeComponent();
 24         }
 25         private static void OnSateSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
 26         {
 27             if (e.NewValue != null)
 28             {
 29                 SateChart sateChart = (SateChart)sender;
 30                 clearSates(sateChart);
 31                 drawSates(sateChart, (IEnumerable<Sate>)e.NewValue);
 32             }
 33         }
 34 
 35         private void bdView_Click(object sender, RoutedEventArgs e)
 36         {
 37             isShowBD = this.bdView.IsChecked == true ? true : false;
 38         }
 39 
 40         private void gpsView_Click(object sender, RoutedEventArgs e)
 41         {
 42             isShowGPS = this.gpsView.IsChecked == true ? true : false;
 43         }
 44 
 45         private void glnssView_Click(object sender, RoutedEventArgs e)
 46         {
 47             isShowGLONASS = this.glnssView.IsChecked == true ? true : false;
 48         }
 49         //画卫星图
 50         private static void drawSates(SateChart sateChart, IEnumerable<Sate> staes)
 51         {
 52             // clearSates();
 53 
 54             foreach (Sate item in staes)
 55             {
 56                 switch (item.SateType)
 57                 {
 58                     case SateTypes.BD: if (isShowBD) addSate(sateChart, item); break;
 59                     case SateTypes.GPS: if (isShowGPS) addSate(sateChart, item); break;
 60                     case SateTypes.GLONASS: if (isShowGLONASS) addSate(sateChart, item); break;
 61                     default: break;
 62                 }
 63             }
 64         }
 65         //在卫星图上添加卫星
 66         private static void addSate(SateChart sateChart, Sate sate)
 67         {
 68             double azimuth = double.Parse(sate.Azimuth);
 69             double elevation = double.Parse(sate.Elevation);
 70             double cosLen = getCosLen(r, elevation);
 71             //卫星图片显示
 72             //Image image = new Image();
 73             //image.Source = new BitmapImage(new Uri("images/satellite2.png", UriKind.Relative));
 74             //image.Width = imageWidth;
 75             //image.Height = imageHeight;
 76             //Canvas.SetTop(image, getY(cosLen, azimuth) - imageHeight/2);
 77             //Canvas.SetLeft(image, getX(cosLen, azimuth) - imageWidth/2);
 78             //myCanvas.Children.Add(image);
 79             Ellipse el = new Ellipse();
 80             el.Width = 3;
 81             el.Height = 3;
 82             Canvas.SetTop(el, getY(cosLen, azimuth) - 1.5);
 83             Canvas.SetLeft(el, getX(cosLen, azimuth) - 1.5);
 84             SolidColorBrush sb;
 85             switch (sate.SateType)
 86             {
 87                 case SateTypes.BD: sb = new SolidColorBrush(Colors.Orange); break;
 88                 case SateTypes.GPS: sb = new SolidColorBrush(Colors.Red); break;
 89                 case SateTypes.GLONASS: sb = new SolidColorBrush(Colors.Blue); break;
 90                 default: sb = new SolidColorBrush(Colors.Orange); break;
 91             }
 92             el.Stroke = sb;
 93             el.Fill = sb;
 94 
 95             TextBlock tb = new TextBlock();
 96             tb.Text = sate.PRN;
 97             tb.Foreground = new SolidColorBrush(Colors.White);
 98             tb.FontSize = 2;
 99             Canvas.SetTop(tb, getY(cosLen, azimuth) + 1);
100             Canvas.SetLeft(tb, getX(cosLen, azimuth) + 1);
101 
102             sateChart.myCanvas.Children.Add(el);
103             sateChart.myCanvas.Children.Add(tb);
104         }
105         //清空图上的卫星
106         private static void clearSates(SateChart sateChart)
107         {
108             int count = sateChart.myCanvas.Children.Count;
109             string[] ellipseNames=new string[]{"e1","e2","e3","e4","e5","e6"};
110             string[] tbNames = new string[] {"tb1","tb2","tb3","tb4","tb5","tb6","tb7","tb8","tb9","tb10","tb11"
111             ,"tb12","tb13","tb14","tb15","tb16","tb17"};
112             for (int i = count - 1; i >= 0; i--)
113             {
114                 Ellipse ep = sateChart.myCanvas.Children[i] as Ellipse;
115                 if (ep != null)
116                 {
117                     if (!ellipseNames.Contains(ep.Name))
118                         sateChart.myCanvas.Children.RemoveAt(i);
119                 }else
120                 {
121                     TextBlock tb = sateChart.myCanvas.Children[i] as TextBlock;
122                     if (tb != null)
123                     { 
124                         if(!tbNames.Contains(tb.Name))
125                             sateChart.myCanvas.Children.RemoveAt(i);
126                     }
127                 }
128 
129             }
130         }
131         //求此点到圆心的距离
132         private static double getCosLen(double r, double elevation)
133         {
134             double x = (1 - elevation / 90) * r;
135             return x;
136         }
137         //求X坐标
138         private static double getX(double cosLen, double azimuth)
139         {
140             double x = Math.Sin(azimuth * Math.PI / 180) * cosLen;
141             return x + r;
142         }
143         //求Y坐标
144         private static double getY(double cosLen, double azimuth)
145         {
146             double y = Math.Cos(azimuth * Math.PI / 180) * cosLen;
147             return r - y;
148         }
149     }
View Code

  

然后就在XAML中绑定数据源,后台写好模拟数据源,想着一切ok了

<local:SateChart x:Name="sateChart" SateSource="{Binding Path=Sates,Mode=OneWay}" />
View Code

 

List<Sate> _sates = new List<Sate>();
        public List<Sate> Sates
        {
            get
            {
                if (_sates == null)
                    _sates = new List<Sate>();
                return _sates;
            }
            set { _sates = value;
            }
        }
public MainWindow()
        {
            InitializeComponent();
            this.DataContext = this;
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            _sates.Clear();
         //   _sates = new List<Sate>();
            Sate sate1 = new Sate() {SateType=SateTypes.BD,Azimuth="50",CNO=21,Elevation="60",PRN="166" };
            Sate sate2 = new Sate() { SateType = SateTypes.BD, Azimuth = "60", CNO = 21, Elevation = "60", PRN = "169" };
            Sate sate3 = new Sate() { SateType = SateTypes.BD, Azimuth = "70", CNO = 21, Elevation = "70", PRN = "180" };
            Sate sate4 = new Sate() { SateType = SateTypes.BD, Azimuth = "80", CNO = 21, Elevation = "80", PRN = "111" };
            Sate sate5 = new Sate() { SateType = SateTypes.BD, Azimuth = "90", CNO = 21, Elevation = "90", PRN = "123" };
            _sates.Add(sate1);
            _sates.Add(sate2);
            _sates.Add(sate3);
            _sates.Add(sate4);
            _sates.Add(sate5);
            OnPropertyChanged("Sates");
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this,new PropertyChangedEventArgs(propertyName));
        }
View Code

 经过调试发现执行OnPropertyChanged("Sates")的时候,却没有触发自定义控件中的自定义依赖属性的OnSateSourceChanged(..)方法,这是为什么呢?自己一番百度没有解决,后来还是把问题发布到最近新加的一个Blend设计群里面,热心的群友果然有经验很多,有人提示说要使用ObservaleCollection,可是我早已试过此方法,并不能解决问题,因为绑定的是我们自定义依赖集合属性,不是ItemsControl的ItemsSource属性。还有人直接道出了有效解决办法,要触发自定义依赖集合属性的更改通知需要先new一下集合,然后再OnPropertyChanged方法,但是为什么会出现这样的问题至今他也没有找到相关资料,只是知道这样能够成功解决问题。

  我本来一开始是考虑继承ItemsControl的,可是没有找到相关的资料,网上大多基本都是继承UserControl的。。。,微软的ItemsControl的ItemsSource属性就可以触发更新通知,我想肯定是有原因的,希望后面有时间好好研究一下。

  问题得以解决,虽然不是最佳的解决办法。下面附上完整的测试用例代码,有兴趣的朋友们可以试验一下我说的情况。有高人知道更好的解决办法,希望不吝赐教。

原文地址:https://www.cnblogs.com/cangyue080180/p/5670649.html