0
votes

I'm working towards making click and drag-able spline curves while learning 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. The interpolated points have a high enough resolution that a WPF Polyline should be fine for display. To clarify, I need the higher resolution output, so using a WPF Beizer is not going to work.

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. 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. 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. 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. So I am a little stuck.

MainWindow.xaml: There is an ItemsControl hosted in a mainCanvas. ItemsControl is bound to a property on the 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. 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. The new PolylineControl is added to the ViewModel in an ObservableCollection This is a stand in until I get to accepting mouse input.

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

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. 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.

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.

<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.

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:

ThumbPoint.cs: Contains a single DP for the center of the point, along with the DragDelta functionality.

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.

<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.

Any help is appreciated

1
Your caption says MVVM yet you haven't tagged it as such, and the code you've posted most definitely isn't MVVM. Which do you want? Because the implementation is very, very different between the two.Mark Feldman
Also you need to create a Minimal, Complete, and Verifiable example, for one thing you're referencing a class called PolylineControl which you haven't included.Mark Feldman
mark- please let me know which parts of this are not MVVM. The view has a view model, which data binds to elements of custom controls... the view doesn't know anything about the controls, and the controls don't know anything about the content their hosting. Additionally, I included every file in complete to recreate the solution in VS. I didn't connect references and name spaces, but anyone capable of answering this question would be able to fill in the missing pieces.Andy Stagg
Also- sorry, it may be "poor mans MVVM", but this is the learning process. Really am just trying to learn, and I really did provide the entire file tree for the solution. After thinking about it, the only reason why I could think that you wouldn't consider this MVVM, is that I'm adding polylines through the main window interface? But then I'm completely unsure how one would add new polylines without some method in the interface to capture the points and add them to the VM collection. I included everything, and it's slightly annoying that you're only able to criticize rather than help.Andy Stagg
Able, yes. Willing to spend the time and effort? Depends on how hard you make it. But to answer your question, MVVM wouldn't use any code-behind (i.e. MenuItem_Click) and wouldn't use the Windows Point class (you should be able to unit-test view models without any Windows libraries present). In fact "proper" MVVM wouldn't even require the use of custom user controls.Mark Feldman

1 Answers

0
votes

Following on from Clemens suggestions, I was able to make it work.

I renamed the Polyline.cs control to eliminate confusion with the standard WPF Polyline Shape class to DynamicPolyline. The class now implements INotifyPropertyChanged, and has DP for the PolylinePoints and a seperate ObservableCollection for a NotifyingPoint class which also implements INotifyPropertyChanged. When DynamicPolyline is initialized, it hooks the CollectionChanged event on the ObserableCollection. 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).

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. Probably will end up using a PathGeometery in the future, but for now, this works.

To address Marks non MVVM concerns.. It's a demo app, sorry I had some code to test things in the code behind. 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:

   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.

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. Only change in here was the bindings for X and Y of the NotifyingPoint.

<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