トップ回答者
階層構造を持つコントロールに追加した依存関係プロパティにデータバインドできない

質問
-
開発環境:VS2013 Pro
.NET Framework 4.5
カスタムコントロールに double と IEnumerable<double> の
依存関係プロパティを追加しました。
さらに IEnumerable の依存関係プロパティを追加し、
自身の配列を指定することで階層構造を持つカスタムコントロールとなるようにしました。
このような状況で、
親となるカスタムコントロールに対するプロパティは
通常通りにデータバインドできました。
しかし、
その下の子供となるカスタムコントロールに対する
double あるいは IEnumerable<double> の依存関係プロパティに対して
データバインドしようとしましたが、
プロパティ変更通知が機能しませんでした。
以下に動作確認用のコードを掲載します。
もしかして XAML の中で <x:Array ・・・> を使用しているのがいけないような気がしていますが、
他のコードで同じことを実現する方法が思い付きませんでした。
どのようにすれば子供のカスタムコントロールの
プロパティ変更通知が機能するのでしょうか。
動作確認に用いたプロジェクトの構造は下図のようになります。
デフォルトの WPF アプリケーションプロジェクトを作成し、
WPF カスタムコントロールをひとつと、
MainViewModel.cs を追加しただけのプロジェクトです。
以下動作確認用に編集した部分のコードです。
カスタムコントロールのコード
namespace Test { using System.Collections; using System.Collections.Generic; using System.Windows; using System.Windows.Controls; public class CustomControl1 : Control { #region Items プロパティ public static readonly DependencyProperty ItemsProperty = DependencyProperty.Register("Items", typeof(IEnumerable), typeof(CustomControl1), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, (s, e) => { (s as CustomControl1).OnItemsChanged(); })); public IEnumerable Items { get { return (IEnumerable)GetValue(ItemsProperty); } set { SetValue(ItemsProperty, value); } } private void OnItemsChanged() { var str = "NULL"; if (Items != null) { str = "[ "; foreach (var obj in Items) { var item = obj as Control; str += item != null ? item.Name + " " : "NULL "; } str += "]"; } System.Diagnostics.Debug.WriteLine("Items changed. " + str); } #endregion Items プロパティ #region Values プロパティ public static readonly DependencyProperty ValuesProperty = DependencyProperty.Register("Values", typeof(IEnumerable<double>), typeof(CustomControl1), new FrameworkPropertyMetadata(new double[] { 1.0, 2.0, 3.0 }, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, (s, e) => { (s as CustomControl1).OnValuesChanged(); })); public IEnumerable<double> Values { get { return (IEnumerable<double>)GetValue(ValuesProperty); } set { SetValue(ValuesProperty, value); } } private void OnValuesChanged() { string valuesString = "NULL"; if (Values != null) { valuesString = "[ "; foreach (var value in Values) { valuesString += value.ToString() + " "; } valuesString += "]"; } System.Diagnostics.Debug.WriteLine("[" + Name + "] ValuesProperty changed. " + valuesString); } #endregion Values プロパティ #region Test プロパティ public static readonly DependencyProperty TestProperty = DependencyProperty.Register("Test", typeof(double), typeof(CustomControl1), new FrameworkPropertyMetadata(default(double), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, (s, e) => { (s as CustomControl1).OnTestChanged(); })); public double Test { get { return (double)GetValue(TestProperty); } set { SetValue(TestProperty, value); } } private void OnTestChanged() { System.Diagnostics.Debug.WriteLine("[" + Name + "] Test changed to " + Test.ToString()); } #endregion Test プロパティ #region コンストラクタ public CustomControl1() { Values = new double[] { 10.0, 20.0, 30.0 }; } static CustomControl1() { DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomControl1), new FrameworkPropertyMetadata(typeof(CustomControl1))); } #endregion コンストラクタ } }
MainWindow.xaml のコード
<Window x:Class="Test.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Test" Title="MainWindow" Height="350" Width="525"> <Window.DataContext> <local:MainViewModel /> </Window.DataContext> <Grid> <StackPanel> <Button Content="click me." Command="{Binding TestCommand}" /> <local:CustomControl1 x:Name="parent" Values="{Binding ParentValues}" Test="{Binding ParentTestValue}"> <local:CustomControl1.Items> <x:Array Type="{x:Type local:CustomControl1}"> <local:CustomControl1 x:Name="child01" Values="{Binding SampleValues1}" Test="{Binding TestValue1}" /> <local:CustomControl1 x:Name="child02" Values="{Binding SampleValues2}" Test="{Binding TestValue2}" /> <local:CustomControl1 x:Name="child03" Values="{Binding SampleValues3}" Test="{Binding TestValue3}" /> </x:Array> </local:CustomControl1.Items> </local:CustomControl1> </StackPanel> </Grid> </Window>
MainViewModel のコード
namespace Test { using System; using System.Collections.Generic; using System.ComponentModel; using System.Windows.Input; public class MainViewModel : INotifyPropertyChanged { #region INotifyPropertyChanged のメンバ public event PropertyChangedEventHandler PropertyChanged; protected virtual void RaisePropertyChanged(string propertyName) { var h = PropertyChanged; if (h != null) h(this, new PropertyChangedEventArgs(propertyName)); } #endregion INotifyPropertyChanged のメンバ private IEnumerable<double> parentValues; public IEnumerable<double> ParentValues { get { return parentValues; } set { parentValues = value; RaisePropertyChanged("ParentValues"); } } private IEnumerable<double> sampleValues1; public IEnumerable<double> SampleValues1 { get { return sampleValues1; } set { sampleValues1 = value; RaisePropertyChanged("SampleValues1"); } } private IEnumerable<double> sampleValues2; public IEnumerable<double> SampleValues2 { get { return sampleValues2; } set { sampleValues2 = value; RaisePropertyChanged("SampleValues2"); } } private IEnumerable<double> sampleValues3; public IEnumerable<double> SampleValues3 { get { return sampleValues3; } set { sampleValues3 = value; RaisePropertyChanged("SampleValues3"); } } private double parentTestValue; public double ParentTestValue { get { return parentTestValue; } set { parentTestValue = value; RaisePropertyChanged("ParentTestValue"); } } private double testValue1; public double TestValue1 { get { return testValue1; } set { testValue1 = value; RaisePropertyChanged("TestValue1"); } } private double testValue2; public double TestValue2 { get { return testValue2; } set { testValue2 = value; RaisePropertyChanged("TestValue2"); } } private double testValue3; public double TestValue3 { get { return testValue3; } set { testValue3 = value; RaisePropertyChanged("TestValue3"); } } public MainViewModel() { ParentValues = new double[] { 111.0, 222.0, 333.0 }; SampleValues1 = new double[] { 100.0, 200.0, 300.0 }; SampleValues2 = new double[] { 400.0, 500.0, 600.0 }; SampleValues3 = new double[] { 700.0, 800.0, 900.0 }; ParentTestValue = 111.0; TestValue1 = 100.0; TestValue2 = 200.0; TestValue3 = 300.0; } private DelegateCommand testCommand; public DelegateCommand TestCommand { get { if (testCommand == null) testCommand = new DelegateCommand(_ => { System.Diagnostics.Debug.WriteLine("TestCommand executed."); ParentValues = new double[] { 1111.0, 2222.0, 3333.0 }; SampleValues1 = new double[] { 1000.0, 2000.0, 3000.0 }; SampleValues2 = new double[] { 4000.0, 5000.0, 6000.0 }; SampleValues3 = new double[] { 7000.0, 8000.0, 9000.0 }; ParentTestValue = 1111.0; TestValue1 = 1000.0; TestValue2 = 2000.0; TestValue3 = 3000.0; }); return testCommand; } } } public class DelegateCommand : ICommand { private Action<object> _execute; private Func<object, bool> _canExecute; public DelegateCommand(Action<object> execute) : this(execute, null) { } public DelegateCommand(Action<object> execute, Func<object, bool> canExecute) { _execute = execute; _canExecute = canExecute; } #region ICommand のメンバ public bool CanExecute(object parameter) { return _canExecute == null ? true : _canExecute(parameter); } public event System.EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } public void Execute(object parameter) { if (_execute != null) _execute(parameter); } #endregion ICommand のメンバ } }
余談になりますが、
TabControl なんかでは <x:Array /> を使用せずに
次のように記述できますよね。
これって ItemsControl を使わずに実現するには
どうればいいんでしょうか。
これができれば解決できる気もするんですが…。
<TabControl> <TabItem Header="1" /> <TabItem Header="2" /> <TabItem Header="3" /> </TabControl>
回答
-
上記色々と変更してみたのですが、Generic.xamlの定義のみで出来そうでした。
やりたいことを理解していなかったらすみません。<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Test"> <Style TargetType="{x:Type local:CustomControl1}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:CustomControl1}"> <StackPanel Orientation="Vertical"><!--add--> <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> <ItemsControl ItemsSource="{Binding Values, RelativeSource={RelativeSource TemplatedParent}}"> <ItemsControl.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding}" /> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </Border> <!--add from--> <Border Margin="10" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> <ItemsControl ItemsSource="{TemplateBinding Items}" /> </Border> <!--add to--> </StackPanel><!--add--> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
- 回答としてマーク 星 睦美 2015年2月24日 4:46
すべての返信
-
こんにちは。
ItemsControlを使わずに実現とありますが、
カスタムコントロールをItemsControl派生にするのもNGですか?- CustomControlの継承元をItemsControlにする
- CustomControl1のItemsプロパティは継承元を使うので削除
- Generic.xamlに子の表示方法を定義
- MainWindowではItemsSourceにバインドさせる
Generic.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Test"> <Style TargetType="{x:Type local:CustomControl1}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:CustomControl1}"> <StackPanel Orientation="Vertical"> <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> <ItemsControl ItemsSource="{Binding Values, RelativeSource={RelativeSource TemplatedParent}}"> <ItemsControl.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding}" /> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </Border> <!--子の表示方法--> <Border Margin="10" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> <ItemsControl ItemsSource="{TemplateBinding ItemsSource}" /> </Border> </StackPanel> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
CustomControl1.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using System.Collections; namespace Test { //public class CustomControl1 : Control public class CustomControl1 : ItemsControl { //#region Items プロパティ //public static readonly DependencyProperty ItemsProperty = DependencyProperty.Register("Items", typeof(IEnumerable), typeof(CustomControl1), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, (s, e) => { (s as CustomControl1).OnItemsChanged(); })); //public IEnumerable Items //{ // get { return (IEnumerable)GetValue(ItemsProperty); } // set { SetValue(ItemsProperty, value); } //} //private void OnItemsChanged() //{ // var str = "NULL"; // if (Items != null) // { // str = "[ "; // foreach (var obj in Items) // { // var item = obj as Control; // str += item != null ? item.Name + " " : "NULL "; // } // str += "]"; // } // System.Diagnostics.Debug.WriteLine("Items changed. " + str); //} //#endregion Items プロパティ #region Values プロパティ public static readonly DependencyProperty ValuesProperty = DependencyProperty.Register("Values", typeof(IEnumerable<double>), typeof(CustomControl1), new FrameworkPropertyMetadata(new double[] { 1.0, 2.0, 3.0 }, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, (s, e) => { (s as CustomControl1).OnValuesChanged(); })); public IEnumerable<double> Values { get { return (IEnumerable<double>)GetValue(ValuesProperty); } set { SetValue(ValuesProperty, value); } } private void OnValuesChanged() { string valuesString = "NULL"; if (Values != null) { valuesString = "[ "; foreach (var value in Values) { valuesString += value.ToString() + " "; } valuesString += "]"; } System.Diagnostics.Debug.WriteLine("[" + Name + "] ValuesProperty changed. " + valuesString); } #endregion Values プロパティ #region Test プロパティ public static readonly DependencyProperty TestProperty = DependencyProperty.Register("Test", typeof(double), typeof(CustomControl1), new FrameworkPropertyMetadata(default(double), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, (s, e) => { (s as CustomControl1).OnTestChanged(); })); public double Test { get { return (double)GetValue(TestProperty); } set { SetValue(TestProperty, value); } } private void OnTestChanged() { System.Diagnostics.Debug.WriteLine("[" + Name + "] Test changed to " + Test.ToString()); } #endregion Test プロパティ #region コンストラクタ public CustomControl1() { Values = new double[] { 10.0, 20.0, 30.0 }; } static CustomControl1() { DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomControl1), new FrameworkPropertyMetadata(typeof(CustomControl1))); } #endregion コンストラクタ } }
MainWindow.xaml
<Window x:Class="Test.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Test" Title="MainWindow" Height="350" Width="525"> <Window.DataContext> <local:MainViewModel /> </Window.DataContext> <Grid> <StackPanel> <Button Content="click me." Command="{Binding TestCommand}" /> <local:CustomControl1 x:Name="parent" Values="{Binding ParentValues}" Test="{Binding ParentTestValue}"> <!--<local:CustomControl1.Items> <x:Array Type="{x:Type local:CustomControl1}"> <local:CustomControl1 x:Name="child01" Values="{Binding SampleValues1}" Test="{Binding TestValue1}" /> <local:CustomControl1 x:Name="child02" Values="{Binding SampleValues2}" Test="{Binding TestValue2}" /> <local:CustomControl1 x:Name="child03" Values="{Binding SampleValues3}" Test="{Binding TestValue3}" /> </x:Array> </local:CustomControl1.Items>--> <local:CustomControl1.ItemsSource> <x:Array Type="{x:Type local:CustomControl1}"> <local:CustomControl1 Background="Yellow" x:Name="child01" Values="{Binding SampleValues1}" Test="{Binding TestValue1}" /> <local:CustomControl1 Background="Blue" x:Name="child02" Values="{Binding SampleValues2}" Test="{Binding TestValue2}" /> <local:CustomControl1 Background="Red" x:Name="child03" Values="{Binding SampleValues3}" Test="{Binding TestValue3}" /> </x:Array> </local:CustomControl1.ItemsSource> </local:CustomControl1> </StackPanel> </Grid> </Window>
#NGであれば代替案検討してみます。
-
上記色々と変更してみたのですが、Generic.xamlの定義のみで出来そうでした。
やりたいことを理解していなかったらすみません。<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Test"> <Style TargetType="{x:Type local:CustomControl1}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:CustomControl1}"> <StackPanel Orientation="Vertical"><!--add--> <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> <ItemsControl ItemsSource="{Binding Values, RelativeSource={RelativeSource TemplatedParent}}"> <ItemsControl.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding}" /> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </Border> <!--add from--> <Border Margin="10" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> <ItemsControl ItemsSource="{TemplateBinding Items}" /> </Border> <!--add to--> </StackPanel><!--add--> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
- 回答としてマーク 星 睦美 2015年2月24日 4:46
-
確かに上記のコードですとうまいこと動作しますねぇ。
うーん、悔しい。
XAML の変更点は子供のコレクションを ItemsSource に渡すようにしただけなので、
ItemsControl の ItemsSource プロパティの定義の仕方なんかに
何かキモの部分があるのでしょうか。
属性を付けるとか特殊なクラスを使うとか…。
でもできれば ItemsControl は継承したくありません。
理由は、ItemTemplate プロパティや ItemContainerStyle プロパティ等は必要ないからです。
Values プロパティには大量のデータを参照させる予定で、
その表示は OnRender() の中でコードから指定するつもりなので、
Generic.xaml での表示指定も必要としません。
つまり Items プロパティの表示方法を、
カスタムコントロールを使う人にいじって欲しくないということです。
もちろん ItemTempalate プロパティ等を持っていても
内側で使用していなければ問題ありませんが、
それなら ItemTemplate プロパティ等は最初から持っていない方がスマートですよね。 -
子も結局CustomControl1のStyleが適用されるのでGeneric.xamlの13行目で定義されているとおり
変更通知を受け取るのではないでしょうか。2件目の回答はGeneric.xamlに子の描画位置がなかったので定義しただけということです。
カスタムコントロールを使う人にいじられないようにするのであれば、
Control継承のままで2件目のようにGeneric.xamlに定義すれば、
すくなくとも外からItemsTemplateやItemContainerStyleは変更できないのではないでしょうか。#いずれにしてもControlを継承している以上はTemplateが存在するので変更しようと思えばできると思いますが。
ただ、WPFのメリットを活かすには1件目の回答のようにItemsControlを継承するべきだと個人的には思います。
- 回答の候補に設定 星 睦美 2015年2月23日 2:38