QA@IT

PolylineクラスのPointsプロパティをViewModelのプロパティへバインドしたがうまく動かない

8649 PV

Livetを利用したWPF 4.5アプリケーションを書いています。

ウィンドウにあるPolylineのPointsへ動的にPointをaddしたいです。

そこで、Polyline自体をViewModelから参照できるようにして、そのPolylineのPointsへPointをaddするとうまく動きました(この方法を「方法A」とします)。

しかし、PolylineのPointsをViewModelのプロパティ(PointCollection)へバインドして、そのViewModelのプロパティに対してPointをaddしたものの、こちらは上手く動きませんでした(この方法を「方法B」とします)。

動作サンプルとして、起動するとウィンドウにランダムに折れ線を描画するアプリを書きました。PolyLineのPointsへランダムな座標のPointを0.2秒毎に追加していきます。

MainWindow.xaml:

<Window x:Class="LivetWPFApplication5.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
        xmlns:v="clr-namespace:LivetWPFApplication5.Views"
        xmlns:vm="clr-namespace:LivetWPFApplication5.ViewModels"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="ContentRendered">
            <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize"/>
        </i:EventTrigger>
        <i:EventTrigger EventName="Closed">
            <l:DataContextDisposeAction/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <Grid>
        <Polyline x:Name="BluePolyLine" Stroke="Blue" Points="{Binding Path=BluePolyLinePoints}"/>
        <Polyline x:Name="RedPolyLine" Stroke="Red"/>
    </Grid>
</Window>

MainWindow.xaml.cs:

using System.Windows;

using LivetWPFApplication5.ViewModels;

namespace LivetWPFApplication5.Views
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            var vm = (MainWindowViewModel)DataContext;
            vm.RedPolyline = RedPolyLine;
        }
    }
}

MainWindowViewModel.cs:

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows.Threading;

using Livet;

namespace LivetWPFApplication5.ViewModels
{
    public class MainWindowViewModel : ViewModel
    {
        private Polyline redPolyLine;
        private PointCollection bluePolyLinePoints = new PointCollection();
        private DispatcherTimer timer = new DispatcherTimer();
        private Random rnd = new Random();

        public void Initialize()
        {
            timer.Interval = new TimeSpan(0, 0, 0, 0, 200);
            timer.Tick += (sender, e) =>
            {
                var x = rnd.NextDouble() * 525;
                var y = rnd.NextDouble() * 350;
                var point = new Point(x, y);
#if true
                // (方法B) こちらでうまく動いて欲しい。
                BluePolyLinePoints.Add(point);
                RaisePropertyChanged("BluePolyLinePoints");
#else
                // (方法A) こちらはうまく動く。
                RedPolyline.Points.Add(point);
#endif
            };
            timer.Start();
        }

        public Polyline RedPolyline
        {
            get { return redPolyLine; }
            set
            {
                if (redPolyLine != value)
                {
                    redPolyLine = value;
                    RaisePropertyChanged();
                }
            }
        }

        public PointCollection BluePolyLinePoints
        {
            get { return bluePolyLinePoints; }
            set
            {
                if (bluePolyLinePoints != value)
                {
                    bluePolyLinePoints = value;
                    RaisePropertyChanged();
                }
            }
        }
    }
}

MainWindowViewModelのInitializeメソッドにある#if true#if falseにすると「方法A」でうまく動作します。

逆に#if trueのままですと「方法B」になり上手く動きません。私としては青い折れ線がどんどんランダムに伸びていくように書いたつもりですが、実際にはPolylineのPointsにはPointが追加され続けますがウィンドウ内のPolylineの描画が変化しません。念の為、アプリ実行中にブレークしてPolylineのPointsを覗きましたが、たしかに追加したPointが入っていました。

StackOverflowに同様の質問があり、そちらの回答を読む限りは問題なく動く(単にバインドすれば動く)ように思うのですが、手元で試すと何が違うのかわかりませんがそのようには動きませんでした。

http://stackoverflow.com/questions/5696820/binding-of-a-polyline-what-am-i-doing-wrong

以上、上手く説明できたかわかりませんが、マズい点などをご指摘いただければ幸いです。よろしくお願いいたします。

回答

細かい質問には答えられないかもしれませんが、とりあえず動作したものを。

Livetに限らず、BindしたCollectionの内容変化を通知しないといけないが、PointCollectionではそれができないので別の物を使う必要があるようです。
ここでは ObservableCollection<Points> を使用することにします。

http://stackoverflow.com/questions/3960101/polyline-using-databinding-and-pointcollection-for-continuous-update

提示していただいたコードをベースに変更を加えていきます。
まず、XAMLの方でDataContextを設定してますが、それを後で作成する検証用ViewModelに差し替えます。

MainWindow.xaml.cs

public MainWindow()
{
   InitializeComponent();
   //var vm = (MainWindowViewModel)DataContext;
   //vm.RedPolyline = RedPolyLine;
   this.DataContext = new InspectViewModel();
}

つづいてそのInspectViewModelを作成します。
面倒だったのでファイルは追加せずに既存のMainWindowViewModel.csファイルに追記してます。

Livetではない場合の書き方もコメントに載せておきました(見づらいかもしれませんが)

MainWindowViewModel.cs

public class InspectViewModel :ViewModel {
// public class InspectViewModel :INotifyPropertyChanged {
    public static PointCollectionConverter pointCollectionConverter;

    private DispatcherTimer timer = new DispatcherTimer();
    private Random rnd = new Random();

    public InspectViewModel() {
        // Livet(ViewModel継承)ではなく、INotifyPropertyChangedで実装する場合
        // は Initializeメソッドの中身をここに。
    }

    public void Initialize() {

        timer.Interval = new TimeSpan(0, 0, 0, 0, 200);
        timer.Tick += (sender, e) => {
            var x = rnd.NextDouble() * 525;
            var y = rnd.NextDouble() * 320;
            var point = new Point(x, y);
            _points.Add(point);


            // Livetの場合
            RaisePropertyChanged("Points");

            // INotifyPropertyChangedの場合
            // PropertyChanged(this, new PropertyChangedEventArgs("Points"));  

        };

        timer.Start();
    }

    private ObservableCollection<Point> _points = new ObservableCollection<Point>();
    public ObservableCollection<Point> Points {
        get { return _points; }
    }

    // INotifyPropertyChangedの場合
    // public event PropertyChangedEventHandler PropertyChanged;
}

ポイントとなるのは、Pointsの型がObservableCollection<Point>であることです。


さて、最終的にXAMLでこのInspectViewModel.Points をバインドするわけですが、これはPointCollectionではないのでそのままではバインドできません。
PointCollectionに変換するコンバーターを用意する必要があります。
http://stackoverflow.com/questions/21618046/wpf-path-binding-to-pointcollection-not-updating-ui

MainWindowViewModel.cs

public class PointCollectionConverter : IValueConverter {

    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; // 不要
    }
}

最後に XAMLを編集してこれらをバインドします。
LivetWPFApplication1 は適宜置き換えてください。

  • xmlns:localが追加されています。
  • <Window.Resources>にコンバータを登録しています。
  • 既存のPolylineはそのままに新しいPolylineをGridに追加しています。
  • DataContextは変えていませんがコンストラクタで変更していますので実際にバインドされるのはInspectViewModelという事に気を付けてください。
<Window x:Class="LivetWPFApplication1.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
        xmlns:v="clr-namespace:LivetWPFApplication1.Views"
        xmlns:vm="clr-namespace:LivetWPFApplication1.ViewModels"
        xmlns:local="clr-namespace:LivetWPFApplication1.ViewModels"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <local:PointCollectionConverter x:Key="pointCollectionConverter"  />
    </Window.Resources>
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>

    <i:Interaction.Triggers>
        <i:EventTrigger EventName="ContentRendered">
            <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize"/>
        </i:EventTrigger>
        <i:EventTrigger EventName="Closed">
            <l:DataContextDisposeAction/>
        </i:EventTrigger>

    </i:Interaction.Triggers>

    <Grid>
        <Polyline x:Name="BluePolyLine" Stroke="Blue" Points="{Binding Path=BluePlolyLinePoints}" />
        <Polyline x:Name="RedPolyLine" Stroke="Red"></Polyline>
        <Polyline Stretch="Fill" Grid.Column="0" Name="polyline" Stroke="Red" Points="{Binding Points, Converter={StaticResource pointCollectionConverter}}">
        </Polyline>
    </Grid>
</Window>

これで期待された動作になると思います。

編集 履歴 (1)
  • わかりやすいご説明ありがとうございました。手元でも実装しましたところ、意図通りの動作になりました。PolylineのPointsに渡すViewModelのプロパティには通知可能なものを使い、最終的にPointsにはコンバータを通した値が渡されるわけですね。

    実はこれまでコンバータの使いどころがピンとこず、つい使用を避けていました。しかし今回のような用途はたしかに納得です。勉強になりました!
    -
ウォッチ

この質問への回答やコメントをメールでお知らせします。