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

  • 質問

  • 開発環境: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>
    

    2015年2月20日 1:05

回答

  • 上記色々と変更してみたのですが、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
    2015年2月20日 1:33
    モデレータ

すべての返信

  • こんにちは。

    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であれば代替案検討してみます。

    2015年2月20日 1:29
    モデレータ
  • 上記色々と変更してみたのですが、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
    2015年2月20日 1:33
    モデレータ
  • 確かに上記のコードですとうまいこと動作しますねぇ。
    うーん、悔しい。

    XAML の変更点は子供のコレクションを ItemsSource に渡すようにしただけなので、
    ItemsControl の ItemsSource プロパティの定義の仕方なんかに
    何かキモの部分があるのでしょうか。
    属性を付けるとか特殊なクラスを使うとか…。


    でもできれば ItemsControl は継承したくありません。
    理由は、ItemTemplate プロパティや ItemContainerStyle プロパティ等は必要ないからです。
    Values プロパティには大量のデータを参照させる予定で、
    その表示は OnRender() の中でコードから指定するつもりなので、
    Generic.xaml での表示指定も必要としません。

    つまり Items プロパティの表示方法を、
    カスタムコントロールを使う人にいじって欲しくないということです。
    もちろん ItemTempalate プロパティ等を持っていても
    内側で使用していなければ問題ありませんが、
    それなら ItemTemplate プロパティ等は最初から持っていない方がスマートですよね。

    2015年2月20日 2:10
  • いくつものコードを掲載していただいてありがとうございます。

    ですが、上記の返信にも記載したように、
    Generic.xaml による表示は考えていませんでしたが、
    なぜ Generic.xaml を上記のように変更するだけで
    子供の Values プロパティの変更通知が機能するようになるのでしょうか。
    2015年2月20日 2:18
  • 子も結局CustomControl1のStyleが適用されるのでGeneric.xamlの13行目で定義されているとおり
    変更通知を受け取るのではないでしょうか。

    2件目の回答はGeneric.xamlに子の描画位置がなかったので定義しただけということです。

    カスタムコントロールを使う人にいじられないようにするのであれば、
    Control継承のままで2件目のようにGeneric.xamlに定義すれば、
    すくなくとも外からItemsTemplateやItemContainerStyleは変更できないのではないでしょうか。

    #いずれにしてもControlを継承している以上はTemplateが存在するので変更しようと思えばできると思いますが。

    ただ、WPFのメリットを活かすには1件目の回答のようにItemsControlを継承するべきだと個人的には思います。

    • 回答の候補に設定 星 睦美 2015年2月23日 2:38
    2015年2月20日 4:22
    モデレータ
  • kisuke0303 さん、こんにちは。
    フォーラム オペレーターの星 睦美です。

    フォーラムで役立つ回答だと思いましたので今回は私のほうでTak1wa さんの回答に[回答としてマーク] させていただきました。もしスレッドで返信を続けたい場合には、遠慮なく[回答としてのマークの解除] ができます。

    では、これからもMSDN フォーラムをお役立てください。

    フォーラム オペレーター 星 睦美 - MSDN Community Support


    • 編集済み 星 睦美 2015年2月24日 4:51 編集
    2015年2月24日 4:50