none
Styleを用いたコレクション型依存プロパティの設定方法について RRS feed

  • 質問

  • 表題の件について質問させていただきます.

    質問内容を要約しますと下記のようになります.

    ・コレクション型依存プロパティを実装し,XAMLでその値を設定できるようになったが,
     Style経由で設定しようとすると「有効な値ではない」という例外が発生する


    Windows Formsの同名コントロールの仕様をおおよそコピーする形で
    WPF版NumericUpDownユーザコントロールを作成しています.

    up/downボタンを押し続けた際の加速条件を設定するための
    プロパティとしてAccelerationsを定義しました.
    その型はList<NumericUpDownAcceleration>です.

    下記の記事を参考にしながら実装を行いました.
    http://msdn.microsoft.com/ja-jp/library/aa970563.aspx

    以下にコードを抜粋します.

    // 依存プロパティ定義
    static readonly DependencyPropertyKey AccelerationsPropertyKey =
        DependencyProperty.RegisterReadOnly("Accelerations",
    					typeof(List<NumericUpDownAcceleration>),
    					typeof(NumericUpDown),
    					new FrameworkPropertyMetadata(new List<NumericUpDownAcceleration>()));
    
    public static readonly DependencyProperty AccelerationsProperty = AccelerationsPropertyKey.DependencyProperty;
    
    // CLRプロパティ定義
    public List<NumericUpDownAcceleration> Accelerations
    {
        get { return (List<NumericUpDownAcceleration>)GetValue(AccelerationsProperty); }
        set { SetValue(AccelerationsPropertyKey, value); }
    }
    
    public NumericUpDown()
    {
        InitializeComponent();
    
        // インスタンスごとに新しいListを生成
        this.SetValue(AccelerationsPropertyKey, new List<NumericUpDownAcceleration>());
    }
    



    上記コードで,次のXAMLは正常に動作しました.

    <v:NumericUpDown>
        <v:NumericUpDown.Accelerations>
            <v:NumericUpDownAcceleration Seconds="3" Increment="10" />
        </v:NumericUpDown.Accelerations>
    </v:NumericUpDown>
    



    しかし,インラインStyleを用いた以下のXAMLを実行すると例外が発生します.

    <v:NumericUpDown>
        <v:NumericUpDown.Style>
            <Style TargetType="{x:Type v:NumericUpDown}">
                <Setter Property="Accelerations">
                    <Setter.Value>
                        <v:NumericUpDownAcceleration Seconds="3" Increment="10" />
                    </Setter.Value>
                </Setter>
            </Style>
        </v:NumericUpDown.Style>
    </v:NumericUpDown>-->
    



    例外の内容を以下に示します(名前空間名を一部消しています).

    ハンドルされていない例外: System.Windows.Markup.XamlParseException:
    'xxx.xxx.xxx.xxx.Views.NumericUpDownAcceleration' は、プロパティ
    'Accelerations' の有効な値ではありません。  マークアップ ファイル 'NumericUpDown
    Sample;component/views/mainview.xaml' 行 55 位置 27 のオブジェクト 'System.Windo
    ws.Setter' にエラーがあります。
    ---> System.ArgumentException:
    'xxx.xxx.xxx.xxx.Views.NumericUpDownAcceleration' は、
    プロパティ 'Accelerations' の有効な値ではありません。


    この問題の原因としていくつか推測してみました.

    1. そもそも依存プロパティの実装が適切でない
    2. Styleの記述方法に誤りがある

    NumericUpDownを数多く使用しているため,
    各種プロパティをStyleで一元管理したいと考えています.
    皆様のお知恵をお貸しいただけますでしょうか.

    よろしくお願いいたします.

    2009年10月5日 6:59

回答

  • Setter.Value はプロパティ自体を設定するもので、コレクションの内部を設定することはできないですね。ですからプロパティが読み取り専用では不可能です。Register の方で登録する必要があります。
    // それから、DependencyPropertyKey の DependencyProperty を public に公開してしまったらあらゆる意味で読み取り専用にならないので意味がありません。
    単純にコレクションを依存関係プロパティにした場合、既定値の問題が出てきます。
    PropertyMetadata で既定値を設定した場合、ご参考になった MSDN の記事にもあるようにコレクションが共有されてしまいます。
    かといって、コンストラクタで SetValue によって初期化した場合、依存関係プロパティ値の優先順位 に従って、Style プロパティの設定よりもローカル値が優先され Style による設定は不可能になります。
    この問題の一つの解決策として、プロパティの初期化は行わず、XAML に記述する際に必ずコレクションのインスタンスを生成する、という方法が考えられます。
    public class Setting : DependencyObject {
    }
    public class SettingCollection : ObservableCollection<Setting> {
    }
    <user:CustomControl>
      <user:CustomControl.Settings>
        <user:SettingCollection>
          <user:Setting />
        </user:SettingCollection>
      </user:CustomControl.Settings>
    </user:CustomControl>
    <user:CustomControl>
      <user:CustomControl.Style>
        <Style TargetType="{x:Type user:CustomControl}">
          <Setter Property="Settings">
            <Setter.Value>
              <user:SettingCollection>
                <user:Setting />
              </user:SettingCollection>
            </Setter.Value>
          </Setter>
        </Style>
      </user:CustomControl.Style>
    </user:CustomControl>
    
    このとき、CustomControl.Settings が List<Setting> だと XAML でインスタンス化できないので(型引数 Setting を指定することができません。.NET 4.0 でできるようになるという話をどこかで見かけたような気もしますが)、必ず SettingCollection のように具象クラスを用意する必要があります。
    難点は、直接プロパティに値を設定するときも必ずコレクションのインスタンス化を明示的に記述する必要がある点でしょう。
    <!-- Settings が null なので実行時例外 -->
    <user:CustomControl>
      <user:CustomControl.Settings>
        <user:Setting />
      </user:CustomControl.Settings>
    </user:CustomControl>
    Acceleration クラスの使い方によっては、Geometry クラス群や Drawing クラス群のような構成を考えても良いかもしれません。
    つまり、AccelerationBase を基底抽象クラスとし、その派生具象として Acceleration クラス及び AccelerationGroup クラスを作成、AccelerationGroup は Acceleration のコレクションを保持する、という形です。
    こうすれば、Acceleration が一つだけなら
    <!-- 直接プロパティに設定 -->
    <user:CustomControl>
      <user:CustomControl.Acceleration>
        <user:Acceleration />
      </user:CustomControl.Acceleration>
    </user:CustomControl>
    
    <!-- Style に設定する場合の Setter -->
    <Setter Property="Settings">
      <Setter.Value>
        <user:Acceleration />
      </Setter.Value>
    </Setter>
    と書け、また複数なら
    <!-- 直接プロパティに設定 -->
    <user:CustomControl>
      <user:CustomControl.Acceleration>
        <user:AccelerationGroup>
          <user:Acceleration />
          <user:Acceleration />
        </user:AccelerationGroup>
      </user:CustomControl.Acceleration>
    </user:CustomControl>
    
    <!-- Style に設定する場合の Setter -->
    <Setter Property="Settings">
      <Setter.Value>
        <user:AccelerationGroup>
          <user:Acceleration />
          <user:Acceleration />
        </user:AccelerationGroup>
      </Setter.Value>
    </Setter>
    と記述できます。複数の Acceleration を書く場合は必ず AccelerationGroup 経由になるため、CustomControl.Acceleration が null で実行時例外という事態は起こりえません。
    // なお、AccelerationGroup には ContentPropertyAttribute がついている前提の XAML コードです。
    • 回答としてマーク 菊地俊介 2009年10月21日 9:40
    2009年10月5日 16:03

すべての返信

  • Setter.Value はプロパティ自体を設定するもので、コレクションの内部を設定することはできないですね。ですからプロパティが読み取り専用では不可能です。Register の方で登録する必要があります。
    // それから、DependencyPropertyKey の DependencyProperty を public に公開してしまったらあらゆる意味で読み取り専用にならないので意味がありません。
    単純にコレクションを依存関係プロパティにした場合、既定値の問題が出てきます。
    PropertyMetadata で既定値を設定した場合、ご参考になった MSDN の記事にもあるようにコレクションが共有されてしまいます。
    かといって、コンストラクタで SetValue によって初期化した場合、依存関係プロパティ値の優先順位 に従って、Style プロパティの設定よりもローカル値が優先され Style による設定は不可能になります。
    この問題の一つの解決策として、プロパティの初期化は行わず、XAML に記述する際に必ずコレクションのインスタンスを生成する、という方法が考えられます。
    public class Setting : DependencyObject {
    }
    public class SettingCollection : ObservableCollection<Setting> {
    }
    <user:CustomControl>
      <user:CustomControl.Settings>
        <user:SettingCollection>
          <user:Setting />
        </user:SettingCollection>
      </user:CustomControl.Settings>
    </user:CustomControl>
    <user:CustomControl>
      <user:CustomControl.Style>
        <Style TargetType="{x:Type user:CustomControl}">
          <Setter Property="Settings">
            <Setter.Value>
              <user:SettingCollection>
                <user:Setting />
              </user:SettingCollection>
            </Setter.Value>
          </Setter>
        </Style>
      </user:CustomControl.Style>
    </user:CustomControl>
    
    このとき、CustomControl.Settings が List<Setting> だと XAML でインスタンス化できないので(型引数 Setting を指定することができません。.NET 4.0 でできるようになるという話をどこかで見かけたような気もしますが)、必ず SettingCollection のように具象クラスを用意する必要があります。
    難点は、直接プロパティに値を設定するときも必ずコレクションのインスタンス化を明示的に記述する必要がある点でしょう。
    <!-- Settings が null なので実行時例外 -->
    <user:CustomControl>
      <user:CustomControl.Settings>
        <user:Setting />
      </user:CustomControl.Settings>
    </user:CustomControl>
    Acceleration クラスの使い方によっては、Geometry クラス群や Drawing クラス群のような構成を考えても良いかもしれません。
    つまり、AccelerationBase を基底抽象クラスとし、その派生具象として Acceleration クラス及び AccelerationGroup クラスを作成、AccelerationGroup は Acceleration のコレクションを保持する、という形です。
    こうすれば、Acceleration が一つだけなら
    <!-- 直接プロパティに設定 -->
    <user:CustomControl>
      <user:CustomControl.Acceleration>
        <user:Acceleration />
      </user:CustomControl.Acceleration>
    </user:CustomControl>
    
    <!-- Style に設定する場合の Setter -->
    <Setter Property="Settings">
      <Setter.Value>
        <user:Acceleration />
      </Setter.Value>
    </Setter>
    と書け、また複数なら
    <!-- 直接プロパティに設定 -->
    <user:CustomControl>
      <user:CustomControl.Acceleration>
        <user:AccelerationGroup>
          <user:Acceleration />
          <user:Acceleration />
        </user:AccelerationGroup>
      </user:CustomControl.Acceleration>
    </user:CustomControl>
    
    <!-- Style に設定する場合の Setter -->
    <Setter Property="Settings">
      <Setter.Value>
        <user:AccelerationGroup>
          <user:Acceleration />
          <user:Acceleration />
        </user:AccelerationGroup>
      </Setter.Value>
    </Setter>
    と記述できます。複数の Acceleration を書く場合は必ず AccelerationGroup 経由になるため、CustomControl.Acceleration が null で実行時例外という事態は起こりえません。
    // なお、AccelerationGroup には ContentPropertyAttribute がついている前提の XAML コードです。
    • 回答としてマーク 菊地俊介 2009年10月21日 9:40
    2009年10月5日 16:03
  • 皆様、こんにちは。

    Hongliangさん、とても詳細な回答ありがとうございます。

    korila-fakes-rilaさん、はじめまして。フォーラムのご利用ありがとうございます。
    その後いかがでしょうか。疑問は解決しましたか?
    有用な情報と思われたため、Hongliangさんの回答へ回答マークをつけさせていただきました。

    今後ともフォーラムをよろしくお願いします。
    それでは!
    2009年10月21日 9:43