使用MVVM将WPF折线数据绑定到ObservableCollection自定义控件

时间:2018-08-17 00:31:25

标签: c# wpf xaml

在学习WPF时,我正在努力制作可单击和可拖动的样条曲线。我已经能够成功地处理纯线段,但是要跳到折线却很难。我有一个用于内插WinForms中曾经使用过的样条曲线的类,因此我使用了鼠标的一些输入单击,这些将成为单击和拖动的拇指。插值点具有足够高的分辨率,因此WPF折线应该适合显示。为了澄清,我需要更高分辨率的输出,因此使用WPF Beizer不能正常工作。

我的轮廓设置非常好-但是我遇到的一个特殊问题是,拖动拇指不会或者a)双向绑定设置不正确,或者b)ObservableCollection无法生成通知。我意识到ObservableCollection仅在添加/删除/清除项目等时通知,而不是各个索引都能够产生通知。我花了最后几个小时进行搜索-找到了一些有前途的想法,但未能正确地将它们连接起来。发布了一些代码,以尝试从ObservableCollection继承并重写ObservableCollection中的OnPropertyChanged方法,但这是受保护的虚拟方法。虽然其他人使用OC中的方法调用将PropertyChanged事件处理程序附加到每个对象,但是我不确定在哪里注入该逻辑。所以我有点卡住了。

MainWindow.xaml: mainCanvas中托管有一个ItemsControl。 ItemsControl绑定到ViewModel上的属性

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <Menu>
        <MenuItem x:Name="menuAddNewPolyline" Header="Add Polyline" Click="MenuItem_Click" />
    </Menu>

    <Canvas x:Name="mainCanvas" Grid.Row="1">

        <ItemsControl x:Name="polylinesItemsControl"
                      ItemsSource="{Binding polylines}"
                      >
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Canvas />
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>

    </Canvas>
</Grid>

MainWindow.Xaml.cs: 非常简单-初始化一个新的视图模型,并将其设置为DataContext。有一个带有“添加折线”项的菜单,该菜单依次初始化一个新的PolylineControl,并在窗口的ActualHeight和ActualWidth内生成三个随机点(使用Thread.Sleep,否则它们在调用之间是相同的)。新的PolylineControl被添加到一个ObservableCollection的ViewModel中,这是我可以接受鼠标输入之前的立场。

public partial class MainWindow : Window
    {
        private ViewModel viewModel;

        public MainWindow()
        {
            InitializeComponent();

            viewModel = new ViewModel();

            DataContext = viewModel;
        }

        private Point GetRandomPoint()
        {
            Random r = new Random();
            return new Point(r.Next(0, (int)this.ActualWidth), r.Next(0, (int)this.ActualHeight));
        }

        private void MenuItem_Click(object sender, RoutedEventArgs e)
        {
            var newPolyline = new PolylineControl.Polyline();
            newPolyline.PolylinePoints.Add(GetRandomPoint());
            Thread.Sleep(100);
            newPolyline.PolylinePoints.Add(GetRandomPoint());
            Thread.Sleep(100);
            newPolyline.PolylinePoints.Add(GetRandomPoint());

            viewModel.polylines.Add(newPolyline);

        }
    }

ViewModel.cs: 绝对注意到这里

public class ViewModel
    {
        public ObservableCollection<PolylineControl.Polyline> polylines { get; set; }

        public ViewModel()
        {
            polylines = new ObservableCollection<PolylineControl.Polyline>();
        }
    }

** PolylineControl:

Polyline.cs:** 包含折线的ObservableCollection的DP。最终,它还将包含插值点和输入点,但是该演示将使用一个点集合。我确实尝试使用INotifyPropertyChanged接口无济于事。

public class Polyline : Control
    {
        public static readonly DependencyProperty PolylinePointsProperty =
           DependencyProperty.Register("PolylinePoints", typeof(ObservableCollection<Point>), typeof(Polyline),
               new FrameworkPropertyMetadata(new ObservableCollection<Point>()));

        public ObservableCollection<Point> PolylinePoints
        {
            get { return (ObservableCollection<Point>)GetValue(PolylinePointsProperty); }
            set { SetValue(PolylinePointsProperty, value); }
        }

        static Polyline()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(Polyline), new FrameworkPropertyMetadata(typeof(Polyline)));
        }
    }

Generic.xaml 包含具有数据绑定折线的画布,以及包含用于ThumbPoint控件的DataTemplate的ItemsControl。

<local:PointCollectionConverter x:Key="PointsConverter"/>

    <Style TargetType="{x:Type local:Polyline}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:Polyline}">
                    <Canvas Background="Transparent">

                        <Polyline x:Name="PART_Polyline"
                                  Stroke="Black"
                                  StrokeThickness="2"
                                  Points="{Binding Path=PolylinePoints,
                                                   RelativeSource={RelativeSource TemplatedParent},
                                                   Converter={StaticResource PointsConverter}}"
                                  >

                        </Polyline>

                        <ItemsControl x:Name="thumbPoints"
                          ItemsSource="{Binding PolylinePoints, RelativeSource={RelativeSource TemplatedParent}}"
                          >
                            <ItemsControl.ItemTemplate>
                                <DataTemplate>
                                    <Canvas>
                                        <tc:ThumbPoint Point="{Binding Path=., Mode=TwoWay}"/>
                                    </Canvas>
                                </DataTemplate>
                            </ItemsControl.ItemTemplate>
                        </ItemsControl>

                    </Canvas>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

PointsCollectionConverter.cs: 包含一个IValueConverter,用于将ObservableCollection转换为PointsCollection。

public class PointCollectionConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value.GetType() == typeof(ObservableCollection<Point>) && targetType == typeof(PointCollection))
            {
                var pointCollection = new PointCollection();

                foreach (var point in value as ObservableCollection<Point>)
                {
                    pointCollection.Add(point);
                }

                return pointCollection;
            }

            return null;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return null;
        }
    }

最后, ThumbPointControl:

ThumbPoint.cs: 包含一个用于点中心的DP,以及DragDelta功能。

public class ThumbPoint : Thumb
    {
        public static readonly DependencyProperty PointProperty =
            DependencyProperty.Register("Point", typeof(Point), typeof(ThumbPoint),
                new FrameworkPropertyMetadata(new Point()));

        public Point Point
        {
            get { return (Point)GetValue(PointProperty); }
            set { SetValue(PointProperty, value); }
        }

        static ThumbPoint()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(ThumbPoint), new FrameworkPropertyMetadata(typeof(ThumbPoint)));
        }

        public ThumbPoint()
        {
            this.DragDelta += new DragDeltaEventHandler(this.OnDragDelta);
        }

        private void OnDragDelta(object sender, DragDeltaEventArgs e)
        {
            this.Point = new Point(this.Point.X + e.HorizontalChange, this.Point.Y + e.VerticalChange);
        }
    }

Generic.xaml: 包含样式和数据绑定的Ellipse边界。

<Style TargetType="{x:Type local:ThumbPoint}">
        <Setter Property="Width" Value="8"/>
        <Setter Property="Height" Value="8"/>
        <Setter Property="Margin" Value="-4"/>
        <Setter Property="Background" Value="Gray" />
        <Setter Property="Canvas.Left" Value="{Binding Path=Point.X, RelativeSource={RelativeSource Self}}" />
        <Setter Property="Canvas.Top" Value="{Binding Path=Point.Y, RelativeSource={RelativeSource Self}}" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:ThumbPoint}">
                    <Ellipse x:Name="PART_Ellipse" 
                             Fill="{TemplateBinding Background}"
                             Width="{TemplateBinding Width}"
                             Height="{TemplateBinding Height}"
                             />
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

Window after the Add Polyline menu item is pressed

该代码用于添加具有三个随机点的折线。

Thumbs moved away from poly line

但是,一旦移动了拇指,折线将不会随之更新。

我有一个工作示例,该示例仅包含一个线段(单击添加线段按钮时,它会多次添加到视图模型中),因此看起来逻辑上应该都是正确的,但是由于引入了ObservableCollection承载折线所需的多个点。

感谢您的帮助

1 个答案:

答案 0 :(得分:0)

根据Clemens的建议,我能够使其正常工作。

我重命名了Polyline.cs控件,以消除与标准WPF折线形状类对DynamicPolyline的混淆。该类现在实现INotifyPropertyChanged,并为PolylinePoints提供了DP,为NotifyingPoint类提供了单独的ObservableCollection,该类也实现了INotifyPropertyChanged。初始化DynamicPolyline时,它将在ObserableCollection上挂接CollectionChanged事件。然后,事件处理程序方法要么将事件处理程序添加到集合中的每个项目,要么根据操作将其删除。每个项目的事件处理程序只需调用SetPolyline,它依次循环遍历InputPoints,将它们添加到新的PointCollection,然后在PART_Polyline上设置Points属性(在OnApplyTemplate方法中创建对它的引用)。

原来,折线上的Points属性不会侦听INotifyPropertyChanged接口,因此无法在Xaml中进行数据绑定。将来可能会最终使用PathGeometery,但是目前可以使用。

要解决Marks非MVVM问题。.这是一个演示应用程序,很抱歉,我有一些代码可以测试后面代码中的内容。关键是能够重用这些控件,并将它们与其他控件组合在一起以用于各种用例,因此与重复代码相比,让它们自己拥有更为有意义。

DynmicPolyline.cs:

   public class DynamicPolyline : Control, INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private void OnPropertyChanged([CallerMemberName] string caller = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(caller));
        }

        public static readonly DependencyProperty PolylinePointsProperty =
            DependencyProperty.Register("PoilylinePoints", typeof(PointCollection), typeof(DynamicPolyline),
                new PropertyMetadata(new PointCollection()));

        public PointCollection PolylinePoints
        {
            get { return (PointCollection)GetValue(PolylinePointsProperty); }
            set { SetValue(PolylinePointsProperty, value); }
        }

        private ObservableCollection<NotifyingPoint> _inputPoints;
        public ObservableCollection<NotifyingPoint> InputPoints
        {
            get { return _inputPoints; }
            set
            {
                _inputPoints = value;
                OnPropertyChanged();
            }
        }

        private void SetPolyline()
        {
            if (polyLine != null && InputPoints.Count >= 2)
            {
                var newCollection = new PointCollection();

                foreach (var point in InputPoints)
                {
                  newCollection.Add(new Point(point.X, point.Y));
                }

                polyLine.Points = newCollection;
            }
        }

        private void InputPoints_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Add)
            {
                foreach (var item in e.NewItems)
                {
                    var point = item as NotifyingPoint;
                    point.PropertyChanged += InputPoints_PropertyChange;
                }
            }
            else if (e.Action == NotifyCollectionChangedAction.Remove)
            {
                foreach (var item in e.OldItems)
                {
                    var point = item as NotifyingPoint;
                    point.PropertyChanged -= InputPoints_PropertyChange;
                }
            }

        }

        private void InputPoints_PropertyChange(object sender, PropertyChangedEventArgs e)
        {
            SetPolyline();
        }


        public DynamicPolyline()
        {
            InputPoints = new ObservableCollection<NotifyingPoint>();
            InputPoints.CollectionChanged += InputPoints_CollectionChanged;
            SetPolyline();
        }

        static DynamicPolyline()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(DynamicPolyline), new FrameworkPropertyMetadata(typeof(DynamicPolyline)));
        }

        private Polyline polyLine;

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            polyLine = this.Template.FindName("PART_Polyline", this) as Polyline;

        }

NotifyingPoint.cs 从数据绑定的ThumbPoint更新X或Y时,引发属性更改事件的简单类。

public class NotifyingPoint : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private void OnPropertyChanged([CallerMemberName] string caller = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(caller));
        }

        public event EventHandler ValueChanged;

        private double _x = 0.0;
        public double X
        {
            get { return _x; }
            set
            {
                _x = value;
                OnPropertyChanged();
                ValueChanged?.Invoke(this, null);
            }
        }

        private double _y = 0.0;
        public double Y
        {
            get { return _y; }
            set
            {
                _y = value;
                OnPropertyChanged();
            }
        }

        public NotifyingPoint()
        {
        }

        public NotifyingPoint(double x, double y)
        {
            X = x;
            Y = y;
        }

        public Point ToPoint()
        {
            return new Point(_x, _y);
        }
    }

最后,为了完整起见,这是控件的 Generic.xaml 。唯一的变化是NotifyingPoint的X和Y的绑定。

<Style TargetType="{x:Type local:DynamicPolyline}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:DynamicPolyline}">
                    <Canvas x:Name="PART_Canvas">

                        <Polyline x:Name="PART_Polyline"
                                  Stroke="Black"
                                  StrokeThickness="2"
                                  />

                        <ItemsControl x:Name="PART_ThumbPointItemsControl"
                                      ItemsSource="{Binding Path=InputPoints, RelativeSource={RelativeSource TemplatedParent}}"
                        >
                            <ItemsControl.ItemTemplate>
                                <DataTemplate>
                                    <Canvas>
                                        <tc:ThumbPoint X="{Binding Path=X, Mode=TwoWay}" Y="{Binding Path=Y, Mode=TwoWay}"/>
                                    </Canvas>
                                </DataTemplate>
                            </ItemsControl.ItemTemplate>
                        </ItemsControl>

                    </Canvas>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

我将Spline类放到SetPolyline方法中,并得到以下结果: Two working click and drag able spline curves