Search code examples
c#wpfdata-bindingpathgeometry

WPF Path shape data binding: she don't update


I'm attempting to bind the Data field on a WPF path object. I've tried two different approaches, but neither approach triggers an updated drawing.

Plan 1:

<Path Data="{Binding SegmentPoints, Converter={StaticResource PointsToBezier}}" ... />

SegmentPoints is an ObservableCollection<Point>. The converter returns a new StreamGeometry object. I expected that if I replaced a point in the collection the thing would redraw. That doesn't work. It does redraw if I trigger a PropertyChanged event for SegmentPoints.

Plan 2:

<Path ...><Path.Data><PathGeometry Figures="{Binding Figures}" .../>...

Then, inside the view model, I update things like my BezierSegment.Point3. Because the Point properties on the various segment types are DependencyProperties, I figured that I could update then and they would trigger and change up the tree. That's apparently not the case either.

So my questions: Is there some way to manually trigger an update on a Path object? (If so, I could potentially use NotifyOnTargetUpdated.) Is there some way to configure the system such that point changes on segments will trigger a redraw?


Solution

  • The binding in your first approach is only updated when the SegmentPoint property changes, i.e. is assigned to a new collection. Therefore it does not need to be an ObservableCollection. Just create a new collection and raise the PropertyChanged event.

    public class ViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
    
        private ICollection<Point> segmentPoints;
    
        public ICollection<Point> SegmentPoints
        {
            get { return segmentPoints; }
            set
            {
                segmentPoints = value;
    
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs("SegmentPoints"));
                }
            }
        }
    }
    

    However, in your second approach the trick is not to bind the PathGeometry's Figure property, but instead directly assign a PathFigureCollection, either in XAML or in code.

    <Canvas Background="Transparent" MouseMove="Canvas_MouseMove">
        <Path Stroke="Blue" StrokeThickness="3">
            <Path.Data>
                <PathGeometry>
                    <PathGeometry.Figures>
                        <PathFigureCollection x:Name="figures"/>
                    </PathGeometry.Figures>
                </PathGeometry>
            </Path.Data>
        </Path>
    </Canvas>
    

    Add segments and modify their properties in code:

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
    
            var segment = new BezierSegment(new Point(100, 0), new Point(200, 300), new Point(300, 100), true);
            var figure = new PathFigure();
            figure.Segments.Add(segment);
            figures.Add(figure);
        }
    
        private void Canvas_MouseMove(object sender, MouseEventArgs e)
        {
            var firstSegment = figures[0].Segments[0] as BezierSegment;
            firstSegment.Point2 = e.GetPosition(sender as IInputElement);
        }
    }
    

    You may also create the Figures property in code, as shown below. The MainWindow class defines a Figures property which is assigned to the Figures property of the PathGeometry.

    <Canvas Background="Transparent" MouseMove="Canvas_MouseMove">
        <Path Stroke="Blue" StrokeThickness="3">
            <Path.Data>
                <PathGeometry x:Name="geometry"/>
            </Path.Data>
        </Path>
    </Canvas>
    
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
    
            var segment = new BezierSegment(new Point(100, 0), new Point(200, 300), new Point(300, 100), true);
            var figure = new PathFigure();
            figure.Segments.Add(segment);
    
            Figures = new PathFigureCollection();
            Figures.Add(figure);
    
            geometry.Figures = Figures;
        }
    
        public PathFigureCollection Figures { get; set; }
    
        private void Canvas_MouseMove(object sender, MouseEventArgs e)
        {
            var firstSegment = Figures[0].Segments[0] as BezierSegment;
            firstSegment.Point2 = e.GetPosition(sender as IInputElement);
        }
    }
    

    Apparently, updates to the path figures won't work when you bind the Figures property. The following binding assigns the figures once, but later changes aren't reflected in the Path.

    // replace
    // geometry.Figures = Figures;
    // by
    BindingOperations.SetBinding(geometry, PathGeometry.FiguresProperty,
        new Binding
        {
            Path = new PropertyPath("Figures"),
            Source = this
        });