もくじ
→https://qiita.com/tera1707/items/4fda73d86eded283ec4f
やりたいこと
まずは、自前のユーザーコントロールを作成して、それがもつプロパティに値をバインドして、ユーザーコントロールを使う側(画面側)から操作する、また、ユーザーコントロール側からなにか値を受け取ったりする、ということをしたい。
で、やりたいことは、
使う側からプロパティを介して値をセットするときに、もしユーザーコントロール側が持っている「正しい値の範囲」からセットされた値が逸脱していた場合に、ユーザーコントロール側でそれを正しい値の範囲に丸めたい。かつ、その丸めた値を使う側のプロパティの値にも反映させたい。
イメージとしては、このような感じ。①から④のようなことをしたい。
実験用画面のイメージ
下記のような、画面(MainWindow)に、簡単なユーザーコントロール(SimpleUserControl)を含むものを作って試す。
実験のイメージとしては、
- 画面左のButtonを押すと、画面が持っているstringのプロパティの文字列のケツに"A"が加えられる。(上の図の①)
- それがユーザーコントロール側のプロパティにも反映され、ユーザーコントロール上のテキストBoxに表示される。(上の図の②)
- セットされた文字列が不正な値(5文字以上)になると、ユーザーコントロール側で丸める処理を行う。(上の図の③)
- 丸めた値が画面側のプロパティにも反映される。(上の図の④)
としようと考えたが、試してみた結果、上の④が、思ったようにいかなかった。
実験コード
画面側コード
<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>
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);
}
}
}
}
ユーザーコントロール側コード
<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>
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
イベントハンドラでも同様だった。)
さらに試しに、画面とユーザーコントロールに、DispText
とMyTextProp
とは関係ないプロパティと依存関係プロパティをもうひとつずつ作ってバインドし、上のような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の意味なしになる)
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/
同じようなことでつまってたっぽい。
※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ソース側のプロパティ両方の値を丸める処理には、これは使えない。