Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF Path Binding to PointCollection not updating UI

I am trying to learn MVVM style updates. Not going so swimmingly.

I'm stuck on updating a simple rectangle that is drawn based upon a change to a Point Collection. On initialization, the UI Updates, but upon a simple change to the Point Collection, the Path is not updated in the UI.

I added a few TextBlocks to make sure the Change event is getting fired, but I am a little lost at this point.

Any help appreciated:

XAML:

<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:ExampleGreg" x:Class="ExampleGreg.MainWindow"
        Title="MainWindow" Height="161.614" Width="324.087">
    <Grid x:Name="gridUser" MouseDown="click_MouseDown" >
        <Canvas x:Name="MeterCanvas" Margin="14,7,30,0" Background="#FFAFAFAF" Height="35" VerticalAlignment="Top">
            <Path Stroke="Black" StrokeThickness="2">
                <Path.Data>
                    <PathGeometry x:Name="geometry"/>
                </Path.Data>
            </Path>
            <Path Stroke="Black" StrokeThickness="2">
                <Path.Data>
                    <PathGeometry x:Name="polylinePwr">
                        <PathGeometry.Transform>
                            <ScaleTransform ScaleX="{Binding ActualWidth, ElementName=MeterCanvas}" ScaleY="{Binding ActualHeight, ElementName=MeterCanvas}" />
                        </PathGeometry.Transform>
                        <PathGeometry.Figures>
                            <PathFigure IsClosed ="True" StartPoint="{Binding Path=thePoints[0]}">
                                <PathFigure.Segments>
                                    <PathSegmentCollection>
                                        <PolyLineSegment Points="{Binding thePoints, UpdateSourceTrigger=PropertyChanged}" />
                                    </PathSegmentCollection>
                                </PathFigure.Segments>
                            </PathFigure>
                        </PathGeometry.Figures>
                    </PathGeometry>
                </Path.Data>
            </Path>
        </Canvas>
        <Label Content="{Binding thePoints[0]}" Margin="14,58,199.6,0" VerticalAlignment="Top" Height="30" BorderThickness="1"/>
        <Label Content="{Binding thePoints[1]}" Margin="14,88,199.6,0" VerticalAlignment="Top" Height="30" BorderThickness="1"/>
        <Label Content="{Binding thePoints[2]}" Margin="165,58,29.6,0" VerticalAlignment="Top" Height="30" BorderThickness="1"/>
        <Label Content="{Binding thePoints[3]}" Margin="165,93,29.6,0" VerticalAlignment="Top" Height="30" BorderThickness="1"/>
    </Grid>
</Window>

MainWindow:

    public partial class MainWindow : Window
{
    private MainViewModel mvm;
    public MainWindow()
    {
        InitializeComponent();
        mvm = new MainViewModel();
        this.DataContext = mvm;

    }

    private void click_MouseDown(object sender, MouseButtonEventArgs e)
    {
        mvm.theText = mvm.theText + ".";
        mvm.ChangePoint(.4);
    }
}

ModelView:

    class MainViewModel : INotifyPropertyChanged
{
    private string _theText = "Initial";
    private PointCollection _points = new PointCollection();
    private PolyLineSegment segment;

    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged(String info)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(info));
        }
    }
    public MainViewModel()
    {
        ChangePoint(0.9);
    }

    public string theText
    {
        get { return _theText; }
        set
        {
            if (_theText != value)
            {
                _theText = value;
                OnPropertyChanged("theText");
            }
        }
    }
    public PointCollection thePoints
    {
        get
        { return _points; }
    }

    public void ChangePoint(double x)
    {
        _points.Clear();
        AddPoint(new Point(0.2, 0.2));
        AddPoint(new Point(0.2, 0.8));
        AddPoint(new Point(x, 0.8));
        AddPoint(new Point(x, 0.2));

        OnPropertyChanged("thePoints");
        _theText = _theText + "!";
        OnPropertyChanged("theText");


    }

    public void AddPoint(Point p)
    {
        _points.Add(p);
    }
}

Thanks for any suggestions (that work :) )

--EDIT PER THE ANSWER BELOW-- I added an IValueConverter Class:

    public class PointCollectionConverter : IValueConverter
{
    #region IValueConverter Members

    public object Convert(object value, System.Type targetType, object parameter, System.Globalization.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, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return null; //not needed
    }

    #endregion
}

And Changed the ModelView to use an Observable Collection ... Updated ModelView:

    class MainViewModel : INotifyPropertyChanged
{
    private string _theText = "Initial";
    private ObservableCollection<Point> _points;
    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged(String info)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(info));
        }
    }

    public MainViewModel()
    {
        _points = new ObservableCollection<Point>();
        _points.Add(new Point(0.2, 0.2));
        _points.Add(new Point(0.2, 0.8));
    }

    public string theText
    {
        get { return _theText; }
        set
        {
            if (_theText != value)
            {
                _theText = value;
                OnPropertyChanged("theText");
            }
        }
    }
    public ObservableCollection<Point> thePoints
    {
        get
        { return _points; }
    }

    double xAdder = 0;
    double y = 0.0;
    public void ChangePoint(double x)
    {
        y = y + .1;
        if (y > .9) { y = .1; xAdder += .1; }
        _points.Add(new Point(x+xAdder, y));
        OnPropertyChanged("thePoints");

        _theText = _theText + "!";
        OnPropertyChanged("theText");

    }

}

And updated XAML:

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:ExampleGreg"  x:Class="ExampleGreg.MainWindow"
    Title="MainWindow" Height="161.614" Width="324.087">
<Window.Resources>
    <local:PointCollectionConverter x:Key="pointCollectionConverter"/>
</Window.Resources>
<Grid x:Name="gridUser" MouseDown="click_MouseDown" >
    <Canvas x:Name="MeterCanvas" Margin="14,7,30,0" Background="#FFAFAFAF" Height="35" VerticalAlignment="Top">
        <Path Stroke="Black" StrokeThickness="2">
            <Path.Data>
                <PathGeometry x:Name="geometry"/>
            </Path.Data>
        </Path>
        <Path Stroke="Black" StrokeThickness="2">
            <Path.Data>
                <PathGeometry x:Name="polylinePwr">
                    <PathGeometry.Transform>
                        <ScaleTransform ScaleX="{Binding ActualWidth, ElementName=MeterCanvas}" ScaleY="{Binding ActualHeight, ElementName=MeterCanvas}" />
                    </PathGeometry.Transform>
                    <PathGeometry.Figures>
                        <PathFigure StartPoint="{Binding Path=thePoints[0], Converter={StaticResource pointCollectionConverter}}">
                            <PathFigure.Segments>
                                <PathSegmentCollection>
                                    <PolyLineSegment Points="{Binding thePoints, Converter={StaticResource pointCollectionConverter}}" />
                                </PathSegmentCollection>
                            </PathFigure.Segments>
                        </PathFigure>
                    </PathGeometry.Figures>
                </PathGeometry>
            </Path.Data>
        </Path>
    </Canvas>
    <Label Content="{Binding thePoints[0]}" Margin="14,58,199.6,0" VerticalAlignment="Top" Height="30" BorderThickness="1"/>
    <Label Content="{Binding thePoints[1]}" Margin="14,88,199.6,0" VerticalAlignment="Top" Height="30" BorderThickness="1"/>
    <Label Content="{Binding thePoints[2]}" Margin="165,58,29.6,0" VerticalAlignment="Top" Height="30" BorderThickness="1"/>
    <Label Content="{Binding thePoints[3]}" Margin="165,93,29.6,0" VerticalAlignment="Top" Height="30" BorderThickness="1"/>
</Grid>

I am not really sure if I will gain any performance improvement, given the IValueConverter creates a new PointCollection Each the ObservableCollection is changed.

THANK YOU everyone for helping me through this. I wanted to paste the complete code as working if any others run into the same thing.

EDIT #2 - After benchmarking the Observable w/ IValueConverter vs. Copying PointCollection to a new PointCollection, the Point Collection seems faster:

    // Option 2 - WAY Faster.
    // Just Use a Points Collection, copy it, and add a point.
    public void AddPoint2(Point pt)
    {
        PointCollection pc = new PointCollection(_points2);
        pc.Add(new Point(pt.X, pt.Y));
        _points2 = pc;
        OnPropertyChanged("thePoints2");
    }

called from

            // 5000 point sinwave for testing
            foreach (Point pt in sinWave) mvm.AddPoint2(pt);
            x++;

If there is a better way, comments appreciated.

like image 529
GregG Avatar asked Feb 07 '14 02:02

GregG


1 Answers

Try replacing the property, rather than clearing+re-populating it.

public void ChangePoint(double x)
{
    var newPoints = new PointCollection();
    newPoints.Add(new Point(0.2, 0.2));
    newPoints.Add(new Point(0.2, 0.8));
    newPoints.Add(new Point(x, 0.8));
    newPoints.Add(new Point(x, 0.2));

    OnPropertyChanged("thePoints");
    _theText = _theText + "!";
    OnPropertyChanged("theText");
}

It seems like bindings sometimes ignore the "PropertyChanged" event if the object reference is the same.

like image 100
McGarnagle Avatar answered Nov 15 '22 08:11

McGarnagle