WPF / Silverlightでネストされた非ビジュアルオブジェクトにDataContextを設定する

画像

WPF用のDXSchedulerを開発する際、 MVVMテンプレートを使用するユーザーからスクリプトを受け取りました。

ユーザー定義オブジェクトは、スケジューラのDataContextプロパティに割り当てられ、XAMLマークアップでは、 バインディング式を使用して、オブジェクトの対応するプロパティへの「バインド」が実行されました。

しかし、問題がありました-スケジューラには、データの設定セットを保存する非ビジュアルストレージオブジェクトが含まれていました。 バインディング式が記述されたフォームでは、ストアオブジェクトのプロパティは更新されませんでした。



以下で、この問題がどのように解決されたかについて詳しく説明します...



この記事では、上記のシナリオを示す簡単なソリューションを提供します。 したがって、MVVMテンプレートの完全な実装で指定されたコードを複雑にしたり、INotifyPropertyChangedインターフェイスを積み上げたりすることはしません。 私たちの仕事は、問題の本質をできるだけ反映した例を簡単にすることです。



それでは、視覚的なコントロールから始めましょう。これはモデルの表現になります。



クラスを見る



ネストされたデータストアのプロパティを含むビジュアルコントロール。 XAMLで作成および割り当てられます。

public class SomeVisualControl : Control { public static readonly DependencyProperty InnerDataStoreProperty = DependencyProperty.Register("InnerDataStore", typeof(DataStore), typeof(SomeVisualControl), new PropertyMetadata(null)); public DataStore InnerDataStore { get { return (DataStore)GetValue(InnerDataStoreProperty); } set { SetValue(InnerDataStoreProperty, value); } } }
      
      







クラスデータストア



XAMLで作成され、SomeVisualControlの内部プロパティとして含まれる非ビジュアルデータストア。



DXSchedulerでは、同様の内部オブジェクトはDependencyObjectの子孫であり、定義によりDataContextを含んでいませんでした。 その結果、このオブジェクトのプロパティで式をバインドできませんでした。 したがって、最初に頭に浮かぶのは、DataContextを含むクラスからこのオブジェクトを継承することであり、問​​題は解決されます。



このようなクラスはFrameworkElementであり、基本クラスとして使用します。



 public class DataStore : FrameworkElement { public static readonly DependencyProperty ConnectionStringProperty = DependencyProperty.Register("ConnectionString", typeof(string), typeof(DataStore), new PropertyMetadata(string.Empty)); public string ConnectionString { get { return (string)GetValue(ConnectionStringProperty); } set { SetValue(ConnectionStringProperty, value); } } }
      
      







次に、ユーザーレベルのオブジェクトを定義します。



モデルクラス



カスタムオブジェクトを定義します。 ConnectionStringプロパティは、内部ビジュアルコントロールストレージのプロパティに「関連付け」られます。



 public class DataStoreModel { public string ConnectionString { get; set; } public DataStoreModel(string connection) { ConnectionString = connection; } }
      
      







ModelViewクラス



ユーザーオブジェクトモデルの表現を定義します。 MVVMテンプレートの要件に従って、このクラスはINotifyPropertyChangedを実装する必要がありますが、この例ではこれは必要ありません。



 public class DataStoreViewModel { DataStoreModel dataStore; public DataStoreViewModel(DataStoreModel dataStore) { if (dataStore == null) throw new ArgumentNullException("dataStore"); this.dataStore = dataStore; } public string ModelConnectionString { get { return dataStore.ConnectionString; } } }
      
      







取得したクラスの図を以下に示します。





そこで、問題を提起します。 内部の非ビジュアルオブジェクトのプロパティをモデルのプロパティに接続することです



アプリケーションに進み、XAMLマークアップで必要なコントロールを作成しましょう

< Window x:Class ="DataContextWpfSample.MainWindow"

xmlns ="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml"

xmlns:local ="clr-namespace:DataContextWpfSample"

Loaded ="Window_Loaded"

Title ="MainWindow" Height ="350" Width ="525" >

< Grid >

< local:SomeVisualControl x:Name ="MyVisualControl" >

< local:SomeVisualControl.InnerDataStore >

< local:DataStore ConnectionString ="{Binding ModelConnectionString}" />

</ local:SomeVisualControl.InnerDataStore >



< local:SomeVisualControl.Template >

< ControlTemplate >

< StackPanel >

< TextBlock Text ="DataStore Connection:" FontWeight ="Bold" />

< TextBlock Text ="{Binding Path=InnerDataStore.ConnectionString, RelativeSource={RelativeSource Mode=TemplatedParent}}" TextWrapping ="Wrap" />

</ StackPanel >

</ ControlTemplate >

</ local:SomeVisualControl.Template >

</ local:SomeVisualControl >

</ Grid >

</ Window >



* This source code was highlighted with Source Code Highlighter .








MyVisualControlという名前をコントロールに設定します-これは、ウィンドウの分離コードファイルからアクセスするために必要です。 表示テンプレートを定義し、興味のあるプロパティを表示して、モデルオブジェクトから正しく受信されたことを確認します。



ウィンドウクラスファイルのモデル初期化コードは次のとおりです。



 public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void Window_Loaded(object sender, RoutedEventArgs e) { string connection = @"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\MyDB.mdb;Persist Security Info=True"; DataStoreModel sourceData = new DataStoreModel(connection); DataStoreViewModel sourceDataModel = new DataStoreViewModel(sourceData); this.MyVisualControl.DataContext = sourceDataModel; }
      
      







特に最後の行に注意してください。DataContextを割り当てると、バインディング式を使用して、コントロールのプロパティをモデルのプロパティに「リンク」できます。



アプリケーションを起動しますが、予想される接続文字列ではなく、空の接続文字列が表示されます。

SNOOPユーティリティを使用して、ストレージオブジェクトのDataContextが割り当てられていないことを確認します。







これは、次の事実によるものと思われます。

DataStoreオブジェクトはビジュアルツリーにないため、親コンテキストは設定されていません



DataContextの目的



したがって、ビジュアルコントロールでDataContextがいつ割り当てられるかを判断し、この値を内部DataStoreに設定する必要があります。



FrameworkContentElementクラスには、 DataContextChangedイベントが含まれています。

これを使用し、簡単なコードを書いて、正しい結果を取得します。



 public class SomeVisualControl : Control { // ... public SomeVisualControl() { this.DataContextChanged += new DependencyPropertyChangedEventHandler(SomeVisualControl_DataContextChanged); } void SomeVisualControl_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e) { InnerDataStore.DataContext = e.NewValue; } }
      
      







残念ながら、1つだけあります...

WPFを使用する場合、このアプローチを非常にうまく使用できます。 問題は、Silverlightの現在のバージョンにはDataContextChangedイベントが含まれていないことです。

また、WPFおよびSLコントロールの一般的なコードを作成しているため、ユニバーサルソリューションを作成する必要がありました。



それでは、ビジュアルコントロールのDataContextがいつ変更されるかをどのようにして知るのでしょうか?



次のアプローチを使用できます...

率直に言って、このアイデアは新しいものではありません。 さまざまなクラスで使用したり、ネストされた非ビジュアルオブジェクトの階層を持つクラスで使用できるように、これを一般化しようとしました。

この考え方の本質は、DependencyPropertyが作成され、バインドが行われることです。作成されたプロパティは、コンテキストの変更について知りたいコントロールのDataContextプロパティに関連付けられます。 この場合、 DependencyPropertyを登録するときに PropertyChangedCallbackを指定する必要があります。 このコールバック関数は、DataContextプロパティの値が変更されたときに呼び出されます。 そして、ここで必要なすべてのオブジェクト(この場合はInnerDataStoreオブジェクト)にコンテキストを割り当てることができます。



今後は、DataContextプロパティが変更され、ネストされたオブジェクトに割り当てる必要があることを報告するインターフェイスを定義すると言います。



 public interface IDataContextOwner { object DataContext { get; } void UpdateInnerDataContext(object dataContext); }
      
      







DataContextおよびPropertyChangedCallbackメソッドのバインディングを含む上記のクラスを実装します。

同時に、クラスのコンストラクターにIDataContextOwnerインターフェイスを実装するオブジェクト(この場合はSomeVisualControl)を渡し、インターフェイスメソッドUpdateInnerDataContextを呼び出して、内部ストレージのコンテキストを更新するタイミングであることをビジュアルコントロールに伝えます。



 public class DataContextBinder : DependencyObject { IDataContextOwner owner; public DataContextBinder(IDataContextOwner owner) { if (owner == null) throw new ArgumentNullException("owner"); this.owner = owner; InitializeBinding(); } protected virtual void InitializeBinding() { Binding binding = new Binding("DataContext"); binding.Source = owner; binding.Mode = BindingMode.OneWay; BindingOperations.SetBinding(this, DataContextProperty, binding); } public object DataContext { get { return (object)GetValue(DataContextProperty); } set { SetValue(DataContextProperty, value); } } public static readonly DependencyProperty DataContextProperty = DependencyProperty.Register("DataContext", typeof(object), typeof(DataContextBinder), new PropertyMetadata(null, new PropertyChangedCallback(OnDataContextChanged))); public static void OnDataContextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((DataContextBinder)d).OnDataContextChanged(e.OldValue, e.NewValue); } private void OnDataContextChanged(object oldValue, object newValue) { owner.UpdateInnerDataContext(newValue); } }
      
      







ビュー(SomeVisualControl)でIDataContextOwnerインターフェイスのかなり単純な実装を記述しましょう。

 public class SomeVisualControl : Control, IDataContextOwner { // ... object IDataContextOwner.DataContext { get { return DataContext; } } void IDataContextOwner.UpdateInnerDataContext(object dataContext) { if (InnerDataStore != null) InnerDataStore.DataContext = dataContext; } }
      
      







最後に行うことは、ビュー内にDataContextBinderのインスタンスを作成することです。

これはクラスコンストラクターで直接実行できます。

 public SomeVisualControl() { this.dataContextBinder = new DataContextBinder(this); }
      
      







アプリケーションを実行し、コンテキストが内部オブジェクトに割り当てられ、モデルからの行がDataStoreオブジェクトに正しく設定されていることを確認します。 これで、アプリケーションウィンドウにユーザー定義モデルのデータが表示されます。







結論



この実装は特定のクラスに関連付けられておらず、「通常の」手段で受信できないオブジェクトにDataContextを割り当てる必要がある場合に適用できます。

同時に、非ビジュアルオブジェクトの階層により深くコンテキストを渡す必要がある場合、単にDataContextBinderクラスのオブジェクトを作成し、IDataContextOwnerインターフェイスを実装します。

これにより、各クラスでの依存関係プロパティの面倒な記述とそれらの間のバインディングの定義から解放されます。 DataContextBinderは機能をカプセル化し、ある時点で、ネストされたオブジェクトに新しいコンテキスト値を設定する必要があることを所有者に通知します。



サンプルのソースはこちらから入手できます。

WPFおよびSilverlight



All Articles