LoginSignup
0
1

More than 1 year has passed since last update.

[xaml/C#]ユーザーコントロールのプロパティの値の妥当性検証と値の矯正のやり方(と、躓いた箇所とその回避策)

Last updated at Posted at 2020-02-20

もくじ
https://qiita.com/tera1707/items/4fda73d86eded283ec4f

やりたいこと

まずは、自前のユーザーコントロールを作成して、それがもつプロパティに値をバインドして、ユーザーコントロールを使う側(画面側)から操作する、また、ユーザーコントロール側からなにか値を受け取ったりする、ということをしたい。

で、やりたいことは、
使う側からプロパティを介して値をセットするときに、もしユーザーコントロール側が持っている「正しい値の範囲」からセットされた値が逸脱していた場合に、ユーザーコントロール側でそれを正しい値の範囲に丸めたい。かつ、その丸めた値を使う側のプロパティの値にも反映させたい。

イメージとしては、このような感じ。①から④のようなことをしたい。
image.png

実験用画面のイメージ

下記のような、画面(MainWindow)に、簡単なユーザーコントロール(SimpleUserControl)を含むものを作って試す。
image.png

実験のイメージとしては、

  1. 画面左のButtonを押すと、画面が持っているstringのプロパティの文字列のケツに"A"が加えられる。(上の図の①)
  2. それがユーザーコントロール側のプロパティにも反映され、ユーザーコントロール上のテキストBoxに表示される。(上の図の②)
  3. セットされた文字列が不正な値(5文字以上)になると、ユーザーコントロール側で丸める処理を行う。(上の図の③)
  4. 丸めた値が画面側のプロパティにも反映される。(上の図の④)

としようと考えたが、試してみた結果、上の④が、思ったようにいかなかった。

実験コード

画面側コード

MainWindow.xaml
<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="250" Width="400"
        x:Name="root">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>

        <Button Content="Button" Click="Button_Click"/>
        <local:SimpleUserControl Grid.Column="1" MyTextProp="{Binding DispText, ElementName=root, Mode=TwoWay}" />
    </Grid>
</Window>

MainWindow.xaml.cs
using System;
using System.ComponentModel;
using System.Windows;

namespace WpfApp1
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string propertyName)
        {
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        // userControlにバインドして文字列を渡すためのプロパティ
        public string DispText
        {
            get { return _dispText; }
            set { Console.WriteLine("DispText = {0}", value); _dispText = value; OnPropertyChanged(nameof(DispText));  }
        }
        private string _dispText = string.Empty;

        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                // "A"を付け足していく
                DispText += "A";
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }
    }
}

ユーザーコントロール側コード

SimpleUserControl.xaml
<UserControl x:Class="WpfApp1.SimpleUserControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             mc:Ignorable="d" 
             x:Name="root">
    <Grid Width="150" Height="50">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition Width="50"/>
        </Grid.ColumnDefinitions>

        <TextBox x:Name="MyTxt" Text="{Binding MyTextProp, ElementName=root, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextChanged="MyTxt_TextChanged" />
        <Button Grid.Column="1" Content="ボタン" />
    </Grid>
</UserControl>
SimpleUserControl.xaml.cs
using System;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;

namespace WpfApp1
{
    public partial class SimpleUserControl : UserControl
    {
        public string MyTextProp
        {
            get { return (string)GetValue(MyTextProperty); }
            set { SetValue(MyTextProperty, value); }
        }
        public static readonly DependencyProperty MyTextProperty =
            DependencyProperty.Register(
                nameof(MyTextProp),                                     // プロパティ名
                typeof(string),                                         // プロパティの型
                typeof(SimpleUserControl),                              // プロパティを所有する型=このクラスの名前
                new PropertyMetadata("",                             // 初期値
                    new PropertyChangedCallback(StringChanged),         // プロパティが変わった時のハンドラ
                    new CoerceValueCallback(CoerceStringValue)),        // 値の矯正のためのハンドラ
                new ValidateValueCallback(ValidateStringValue));        // 値の妥当性確認のためのハンドラ

        // 値の変化 
        private static void StringChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Console.WriteLine(MethodBase.GetCurrentMethod().Name + " old : " + e.OldValue + " new : " + e.NewValue);
            // 値がかわらないとここは通らない。(プロパティに値を入れても、同じ値だとここは通らない)

            // ※値が変わったときになにか画面上の表示を変えたい、とかあれば、
            // 下記のようにすればいい。(staticメソッドなので、そのままNameを使うとかができない)
            //if (d is SimpleUserControl ctrl)
            //{
            //    ctrl.コントロールの名前.Text = "あいうえお";
            //}

        }
        // 値の矯正
        private static object CoerceStringValue(DependencyObject d, object baseValue)
        {
            Console.WriteLine(MethodBase.GetCurrentMethod().Name + " value : " + (string)baseValue);

            var txt = (string)baseValue;
            return (txt.Length <= 5) ? txt : string.Empty;   // 5文字以上なら空文字に矯正する(※が、画面側には伝わらない!)
        }
        // 値の妥当性確認
        private static bool ValidateStringValue(object value)
        {
            var txt = (string)value;
            Console.WriteLine(MethodBase.GetCurrentMethod().Name + " value : " + txt); 

            if (txt == null) return false;                          // nullのときは異常(falseをreturnすると、ArgumentExceptionを返してくれる)
            if (txt.Length >= 5) throw new InvalidCastException();  // 5文字以上なら自分の好きな例外をスローしてやる
            return true;                                            // それ以外はOKとする(setされた値になる)
        }
        // コンストラクタ
        public SimpleUserControl()
        {
            InitializeComponent();
        }
        // テキストが変化したときのイベント
        private void MyTxt_TextChanged(object sender, TextChangedEventArgs e)
        {
            Console.WriteLine(MethodBase.GetCurrentMethod().Name);
            // (仮にここでMyTextPropを書き換えたとしても画面側(DispText)には伝わらない!!!!)
        }
    }
}

やり方

ユーザーコントロール側では、画面側に向けたインターフェースとしてDependencyPropertyを使う。(これはもうそういうものだとして覚える)

そのDependencyPropertyには、セットされた値に対して

  • 検証(値が正しい値なのかどうか)
  • 矯正(値を正しい値に書き換える(強制??))
  • 変化検出(値が変わったことを検出する)

ということができるような仕組みが用意されている。それぞれ、

  • ValidateValueCallback
  • CoerceValueCallback
  • PropertyChangedCallback

というもの。上のサンプルでは、SimpleUserControl.xaml.csの中で定義している依存関係プロパティMyTextPropertyを作るときに使われている。(使い方は実験コードのコメントの通り)

うまくいかなかった点

上のほうの図の中の、

  • ①値をセットする
  • ②セットされた値がユーザーコントロールのプロパティに伝わる
  • ③セットされた値が不正だった場合に値を丸める(範囲内の値に直す)
  • ④丸められた値が画面側のプロパティにも反映される

のうち、①②③まではうまくいった。(③は、途中まで?うまくいった)が、④がうまくいかなかった。

上で挙げた仕組みのどこかで値を丸める(=サンプル中のMyTextPropを丸める)と、MyTextPropの値は意図通り丸めることができるのだが、それが画面側のプロパティ(=サンプル中のDispText)に反映されてくれない。

通常は、上のほうの図のようにバインドしたときは、ユーザーコントロール側でMyTextPropの値に何か入れてやると、画面側のDispTextも一緒に変化してくれる。が、今回の場合はそうはならなかった。

色々試したところ、

  • ユーザーコントロールの依存関係プロパティにバインドした画面側のプロパティは、そのプロパティの値を操作したときの「検証/矯正/変化検出」の仕組み(メソッド)の中では書き換えることができない

っぽい。(変化検出というのは、PropertyChangedCallbackで設定したメソッドもだが、試したところ、TextBoxのTextChangedイベントハンドラでも同様だった。)

さらに試しに、画面とユーザーコントロールに、DispTextMyTextPropとは関係ないプロパティと依存関係プロパティをもうひとつずつ作ってバインドし、上のようなMyTextPropの変化の流れの中で値を書き換えてやると、うまく画面側のプロパティにも書き換えた値が反映された。

まとめ

うまくまとめられないが、

  • ユーザーコントロールの依存関係プロパティの「検証/矯正/変化検出」の仕組みでは、それにバインドしたユーザーコントロールを使う側のプロパティまでは矯正(強制?)を行うことはできない。(っぽい)

その後

結果、上記のように丸められなかったので、実験コードのValidateStringValue()メソッドに書いた処理のように、
異常な値の場合は例外を吐くようにして、画面側で「異常な値」をsetしたところにお知らせする ようにした。
(これが正しいやり方なのかどうかはわからないが...)

参考

妥当性検証など
https://blog.okazuki.jp/entry/2014/08/17/220810

[C#][WPF]DependencyObjectって その2
http://blogs.wankuma.com/kazuki/archive/2008/01/29/119892.aspx

追記(ValidateValueCallbackの中でfalseを返した時の動きについて)

実際に動かして試したところ、ValidateValueCallbackの中でreturn falseすると、
ArgumentException例外を投げてくれるわけではなくプロパティの値が初期値に戻る(PropertyMetadataの第一引数)ような動きをしている。
その際、デバッグ出力の欄には

System.Windows.Data Error: 5 : Value produced by BindingExpression is not valid for target property.; Value='AAAAAAA' BindingExpression:Path=DispText; DataItem='MainWindow' (Name='root'); target element is 'SimpleUserControl' (Name='root'); target property is 'MyTextProp' (type 'String')

というMsgが出ている。
例外は投げないが、異常値なので初期値に戻すということ?詳細は調べきれてない。

追記(ごりごりの逃げの方法)

どうしても親画面側のプロパティの値を一緒に丸めてしまいたいときは、下記のようにして無理やりできるのはできた。
(親のプロパティ名を指定してしまっているので、依存性が出来てしまってUserControlの意味なしになる)

逃げ.cs
private static void StringChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    BindingExpression beb = BindingOperations.GetBindingExpression(d, MyTextProperty);
    if (beb != null)
    {
        if (((string)e.NewValue).Length >= 5)
        {
            (beb.DataItem as MainWindow).DispText = (string)"";
        }
    }
}

もう他でそのUserControlを絶対使わなくて、どうしても今すぐそういうことがしたいときはこれで逃げれるかも。

追記2(超ごりごりの逃げの方法)

PropertyChangeハンドラの中で1msのDispatcherTimerをかけて、TImerTimckの中で、そのプロパティの値を入れなおしてやると、画面側のプロパティも、ユーザーコントロール側のプロパティの値になってくれる。
※一応、動き的にはそれっぽく動く。
→やっぱり、自分の変化等ハンドラ内でなければ、普通に値をセットしたらソース側に反映される。、 が、絶対なにか副作用がありそう、、、

参考

MSDOCS 依存関係プロパティとは?
https://docs.microsoft.com/ja-jp/windows/uwp/xaml-platform/custom-dependency-properties

XAMLコードから生成されるプログラム・コードを理解する
依存関係プロパティを結構詳しく解説してくれている
https://www.atmarkit.co.jp/ait/articles/1008/03/news097_3.html

WPFの解説、結構わかりやすい
https://www.atmarkit.co.jp/ait/series/2794/

さんこう
https://stackoverflow.com/questions/3894016/how-do-you-expose-a-dependency-property-of-a-private-internal-object-via-the-in

同じようなことでつまってたっぽい。
※PropertyChangeで自分に自分の値を入れたらいけた、とあるが、試したらダメだったが。。。
http://var.blog.jp/archives/67898237.html

下記ページ、自分が実験して「こういう動きするのかな?」と感じたことが、全部書かれてる。超有用。
http://bbs.wankuma.com/index.cgi?mode=al2&namber=36371&KLOG=63
→□投稿者/ Hongliang (402回)-(2009/05/29(Fri) 07:13:00)
 CoeceValueの矯正の結果が、bindingsourceに伝わらないのは仕様バグっぽいとのこと
→□投稿者/ 囚人 (365回)-(2009/05/29(Fri) 07:19:34)
 CLRプロパティでは、setvalue,getvalue以外のことをしないほうがよい
→□投稿者/ Hongliang (402回)-(2009/05/29(Fri) 07:13:00)
 また双方向バインディングでは当然ながらバインディングソースからの設定に対して CLR プロパティでの値チェックは無力です。
 直接 SetValue が呼び出されますからね
⇒つまり、 CoeceValueで強制した値はbindingソース側に伝えることができない。(自分で試した限り、PropertyChangedCallbackも一緒。ValidateValueCallbackでfalse返しても、bindingソース側はかわらない)
 イコール、UserContorl内のプロパティとbindibgソース側のプロパティ両方の値を丸める処理には、これは使えない。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1