まめ - たんたんめん

備忘録 C# / WPF 多め

WPF DependencyProperty SetCurrentValue と SetValueの違い

今日は DependencyProperty.SetValue()とDependencyPropertySetCurrentValue()の違いについて具体例を出して挙動の違いを見ていこうと思います。
「ViewModelの値とViewの値が違うんです。」みたいな相談をたまに受けるんですが大体原因はこれです。

まずはDependencyProperyですが

        public static readonly DependencyProperty PropertyProperty = DependencyProperty.Register(
            "Property", typeof(string), typeof(MainWindow), new PropertyMetadata(default(string)));

        public string Property
        {
            get { return (string) GetValue(PropertyProperty); }
            set { SetValue(PropertyProperty, value); }
        }

        void Func()
       {
             Property = "hoge";
       }

てこんなやつですね。

'Property = "hoge" 'を呼びだすと内部のSetValue()からCLR層に設定しに行きます。 今回はこのSetValueが問題になるケースがあるのでこれを解説します。

とりあえずサンプルソースコードです。なるべくシンプルにしています。

f:id:at12k313:20200323161643p:plain

MainWindow.xaml

<Window x:Class="Binding_Sample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        
        <TextBlock Text="View Value = "/>
        <TextBlock x:Name="block1" Text="{Binding Text}" Margin="0,0,0,20"/>
            
        <TextBlock Text="ViewModel Value = "/>
        <TextBlock Text="{Binding Text}" Margin="0,0,0,20"/>
            
        <Button x:Name="button1" Content="ビューの値変更"/>
        
        <Separator/>
        
        <TextBlock Text="View Value = " FontSize="20"/>
        <TextBlock x:Name="block2" Text="{Binding Text2}" Margin="0,0,0,20"/>
            
        <TextBlock Text="ViewModel Value = " FontSize="20"/>
        <TextBlock Text="{Binding Text2}" Margin="0,0,0,20"/>
            
        <Button x:Name="button2" Content="ビューの値変更"/>
    </StackPanel>
</Window>

MainWindow.xaml.cs

using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows.Controls;

namespace Binding_Sample
{
   // VIewModelです。
    public class Context : INotifyPropertyChanged
    {
        private string _text = "ビューモデルから設定";

        public string Text
        {
            get => _text;
            set => SetProperty(ref _text, value);
        }
        
        private string _text2 = "ビューモデルから設定";

        public string Text2
        {
            get => _text2;
            set => SetProperty(ref _text2, value);
        }
        
        private void SetProperty<T>(ref T source, T value, [CallerMemberName] string propertyName = null)
        {
            source = value;
            RaisePropertyChanged(propertyName);
        }
        public event PropertyChangedEventHandler PropertyChanged;

        private void RaisePropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public partial class MainWindow
    {
        public MainWindow()
        {
            var context = new Context();
            DataContext = context;
            
            InitializeComponent();
            
            // ボタンのコードビハインドです。
            button1.Click += async (s, e) =>
            {
               // 値を設定し
                block1.Text = "ビューから設定";
                
               // 500ms 待ちます。
                await Task.Delay(500);

               // ViewModelから値を再設定します。
                context.Text = "ビューモデルから設定";
            };
            
            button2.Click += async (s, e) =>
            {
               // 値を設定し
                block2.SetCurrentValue(TextBlock.TextProperty,"ビューから設定");

               // 500ms 待ちます。
                await Task.Delay(500);

               // ViewModelから値を再設定します。
                context.Text2 = "ビューモデルから設定";
            };
        }
    }
}

このソースコードはそれぞれボタンをクリックしたら、TextBlockのTextプロパティを更新しその後500ミリ秒後にViewModelから値を再設定し更新通知を飛ばすプログラムです。 両者の違いは上は SetValue 下は、SetCurrentValueを利用していることです。

実際に動かしてみると以下の様になります。 f:id:at12k313:20200323161922g:plain

上のボタンは1回だけ反応してその後は動いていません。 下のボタンは押すたびにビュー→ビューモデルと値が更新されているのがわかります。

何が起こっているのかというと、SetCurrentValueを使わない場合はSetValueのタイミングでOneWayでBindされていたBindingObjectを上書きしてしまっており、以降ViewModelからの更新通知が飛んできても反映されなくなります。
SetCurrentValueを利用する場合は画面の値を更新しますが、BIndingやDataTrigger等を破壊しないので再度BInd元から更新通知が飛んでくれば元に戻るわけです。

更にややこしいことにTwoWayバインドだとSetValueでも問題無いのです。Bindigを破壊せずソース側( ViewModel)に値がちゃんと飛んできます。

まぁ、独自コントロールを作るときにしかハマらないとは思うのですが覚えておくと困ったときに役立つかもしれません。