简体   繁体   English

WPF折线数据绑定到带有MVVM的ObservableCollection自定义控件

[英]WPF Polyline databound to ObservableCollection Custom Control with MVVM

I'm working towards making click and drag-able spline curves while learning WPF. 在学习WPF时,我正在努力制作可单击和可拖动的样条曲线。 I've been able to successfully work with pure Line segments, but making the jump to a polyline is proving difficult. 我已经能够成功地处理纯线段,但是要跳到折线却很难。 I have a class for interpolating the spline curves that I used to use in WinForms, so I'm using a few input clicks from the mouse, and those will be the thumbs to click and drag. 我有一类用于插值我以前在WinForms中使用的样条曲线的类,因此我使用了鼠标的一些输入单击,这些将成为单击和拖动的拇指。 The interpolated points have a high enough resolution that a WPF Polyline should be fine for display. 插值点具有足够高的分辨率,因此WPF折线应该适合显示。 To clarify, I need the higher resolution output, so using a WPF Beizer is not going to work. 为了澄清,我需要更高分辨率的输出,因此使用WPF Beizer不能正常工作。

I have the outline pretty well setup- but the particular issue I'm having, is that dragging the thumbs does not either a) the two way binding is not setup correctly, or b) the ObservableCollection is not generating notifications. 我的轮廓设置非常好-但我遇到的特殊问题是,拖动拇指不会或者a)双向绑定设置不正确,或者b)ObservableCollection无法生成通知。 I realize that the ObservableCollection only notifies when items are added/removed/cleared, etc, and not that the individual indices are able to produce notifications. 我意识到ObservableCollection仅在添加/删除/清除项目等时通知,而不是各个索引都能够产生通知。 I have spent the last few hours searching- found some promising ideas, but haven't been able to wire them up correctly. 我花了最后几个小时进行搜索-找到了一些有前途的想法,但未能正确地将它们连接起来。 There was some code posted to try inherit from ObservableCollection and override the OnPropertyChanged method in the ObservableCollection, but that's a protected virtual method. 发布了一些代码,以尝试从ObservableCollection继承并重写ObservableCollection中的OnPropertyChanged方法,但这是受保护的虚拟方法。 While others used a method call into the OC to attach PropertyChanged event handlers to each object, but I'm unsure where to inject that logic. 虽然其他人使用OC中的方法调用将PropertyChanged事件处理程序附加到每个对象,但是我不确定在哪里注入该逻辑。 So I am a little stuck. 所以我有点卡住。

MainWindow.xaml: There is an ItemsControl hosted in a mainCanvas. MainWindow.xaml: mainCanvas中托管有一个ItemsControl。 ItemsControl is bound to a property on the ViewModel 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: Pretty simple- initializes a new view model, and it's set as the DataContext. MainWindow.Xaml.cs:非常简单-初始化一个新的视图模型,并将其设置为DataContext。 There is a menu with a Add Polyline item, which in turn, initializes a new PolylineControl, and generates three random points (using Thread.Sleep, otherwise they were the same, between the calls) within the ActualHeight and ActualWidth of the window. 有一个带有“添加折线”项的菜单,该菜单又会初始化一个新的PolylineControl,并在窗口的ActualHeight和ActualWidth内生成三个随机点(使用Thread.Sleep,否则它们在调用之间是相同的)。 The new PolylineControl is added to the ViewModel in an ObservableCollection This is a stand in until I get to accepting mouse input. 新的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: Absolutely noting fancy here ViewModel.cs:绝对在这里注意到

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

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

**The PolylineControl: **折线控件:

Polyline.cs:** Contains DP's for an ObservableCollection of points for the polyline. Polyline.cs:**包含DP的折线点的ObservableCollection。 Eventually this will also contain the interpolated points as well as the input points, but a single collection of points will do for the demo. 最终,它还将包含插值点和输入点,但是该演示将使用一个点集合。 I did try to use the INotifyPropertyChanged interface to no avail. 我确实尝试使用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 Contains a canvas with a databound Polyline, and an ItemsControl with a DataTemplate for the ThumbPoint control. 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: Contains a IValueConverter to turn the ObservableCollection into a PointsCollection. 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;
        }
    }

And finally, the ThumbPointControl: 最后, ThumbPointControl:

ThumbPoint.cs: Contains a single DP for the center of the point, along with the DragDelta functionality. 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: Contains the style, and an Ellipse bound which is databound. 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 按下添加折线菜单项后的窗口

The code works to add the polyline with three random points. 该代码可为折线添加三个随机点。

Thumbs moved away from poly line 拇指从折线移开

However, once you move the thumbs, the polyline does not update along with it. 但是,一旦移动了拇指,折线将不会随之更新。

I have a working example of just a single line segment (added to the view model as many times as you click the add segment button) so it seems the logic should all be correct, but something broke down with the introduction of the ObservableCollection to host the multiple points required for a polyline. 我有一个仅行线段的工作示例(单击添加线段按钮时,它会多次添加到视图模型中),因此逻辑似乎都应该是正确的,但是由于将ObservableCollection引入主机而出现了问题折线所需的多个点。

Any help is appreciated 任何帮助表示赞赏

Following on from Clemens suggestions, I was able to make it work. 遵循Clemens的建议,我得以使其工作。

I renamed the Polyline.cs control to eliminate confusion with the standard WPF Polyline Shape class to DynamicPolyline. 我重命名了Polyline.cs控件,以消除与标准WPF折线形状类对DynamicPolyline的混淆。 The class now implements INotifyPropertyChanged, and has DP for the PolylinePoints and a seperate ObservableCollection for a NotifyingPoint class which also implements INotifyPropertyChanged. 该类现在实现INotifyPropertyChanged,并为PolylinePoints提供了DP,为NotifyingPoint类提供了单独的ObservableCollection,该类也实现了INotifyPropertyChanged。 When DynamicPolyline is initialized, it hooks the CollectionChanged event on the ObserableCollection. 初始化DynamicPolyline时,它将在ObserableCollection上挂接CollectionChanged事件。 The event handler method then either adds an event handler to each item in the collection, or removes it based on the action. 然后,事件处理程序方法要么将事件处理程序添加到集合中的每个项目,要么根据操作将其删除。 The event handler for each item simply calls SetPolyline, which in turn cycles through the InputPoints adding them to a new PointCollection, and then sets the Points property on the PART_Polyline (which a reference to is created in the OnApplyTemplate method). 每个项目的事件处理程序仅调用SetPolyline,它依次循环遍历InputPoints,将它们添加到新的PointCollection,然后在PART_Polyline上设置Points属性(在OnApplyTemplate方法中创建对它的引用)。

It turns out the Points property on a Polyline does not listen to the INotifyPropertyChanged interface, so data binding in the Xaml was not possible. 原来,折线上的Points属性不会侦听INotifyPropertyChanged接口,因此无法在Xaml中进行数据绑定。 Probably will end up using a PathGeometery in the future, but for now, this works. 将来可能最终会使用PathGeometery,但是目前为止,这可行。

To address Marks non MVVM concerns.. It's a demo app, sorry I had some code to test things in the code behind. 为了解决Marks非MVVM问题。.这是一个演示应用程序,很抱歉,我有一些代码可以测试后面的代码中的内容。 The point is to be able to reuse these controls, and group them with others for various use cases, so it makes more sense for them to be on their own vs repeating the code. 关键是要能够重用这些控件,并针对各种用例将它们与其他控件组合在一起,因此与重复代码相比,让它们自己拥有更有意义。

DynmicPolyline.cs: 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 Simple class that raises property changed events when X or Y is updated from the databound ThumbPoint. 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);
        }
    }

And finally, for completeness, here is the Generic.xaml for the control. 最后,为了完整起见 ,这里是控件的Generic.xaml Only change in here was the bindings for X and Y of the NotifyingPoint. 此处唯一的更改是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>

I dropped my Spline class in to the SetPolyline method, and got the result I was after: Two working click and drag able spline curves 我将Spline类放到SetPolyline方法中,得到的结果是: 两个有效的单击并拖动可绘制样条曲线

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM