C# のコードでデータ バインディング (4)

C# のコードでデータ バインディング (1) の中で Binding Source の条件について書きましたが、
INotifyPropertyChanged でも DependencyObject でもないオブジェクトの場合は少し難解な動作をします。
というのも、プロパティ変更通知ができないはずのオブジェクトに対して、
内部で PropertyDescriptor を経由して双方向のバインディングができるように試みるためです。

POCO である Person0 クラスを利用したコードを以下に示します。

Binding Source 側を変更しようとするとき、通常の

person.Name = "Jiro";

というコードでは変更は通知されません。
変更を通知するには PropertyDescriptor.SetValue メソッドを使います。
なお、変更時のイベントハンドラーを登録するには PropertyDescriptor.AddValueChanged メソッドを使います。

Binding オブジェクトの内部では OneTime バインディングの場合を除き、
PropertyDescriptor.SetValue メソッドおよび PropertyDescriptor.AddValueChanged メソッドを利用して、
プロパティ変更通知を試みます。

このように、オブジェクトへの操作は PropertyDescriptor を経由しなければならなくなるため、
WPF などのアプリケーションでこの方法でコードを書くのは大変です。
また、アプリケーションのコードで person への参照を削除しても、
PropertyDescriptor の中で person への参照を持ち続けるため、メモリリークの原因となるようです。
したがって、Binding Source として使うオブジェクトには INotifyPropertyChanged インターフェイスを実装させるのがよいでしょう。

補足として、PropertyDescriptor の挙動を確認するためのコードを以下に示します。

 

前回: C# のコードでデータ バインディング (3)

作成したサンプル
BindingConsole (GitHub)
PocoBindingWpf (GitHub)

バージョン情報
C# 6.0
.NET Framework 4.5

参照
【WPF】変更通知をサポートしないCLRプロパティの変更通知
【WPF】PropertyDescriptorを取得 / 利用する。

広告

プロパティ変更をタイマーで同期して通知する

C# のコードでデータ バインディング (1) で書いた通り、
Binding Source は INotifyPropertyChanged か DependencyObject でなければ
プロパティ変更通知ができないため、永続的に値が反映される OneWay バインディングはできません。

そこで、プロパティ変更通知機能を持たない既存のオブジェクトをタイマーで監視して
プロパティ変更通知を発生させる DynamicSyncProxy<T> クラスというのを作ってみました。

DynamicObject クラスを継承し、既存のオブジェクトを内部に持ちます。
そのオブジェクトのプロパティへのアクセスを透過するプロキシとして機能します。

さて、プロパティ変更通知機能を持たない TextModel クラスを用意して、
DynamicSyncProxy<T> を介してデータ バインディングを設定します (dynamic 型として扱います)。

この例では 0.3 秒ごとに値が同期されます。

DynamicBinding

 

前回: ExpandoObject を使ったデータ バインディング
次回: XAML マークアップ拡張を自作する

作成したサンプル
DynamicBindingWpf (GitHub)

バージョン情報
C# 6.0
.NET Framework 4.5

参照
データ バインディングの概要
C# のコードでデータ バインディング (1)

ExpandoObject を使ったデータ バインディング

C# のコードでデータ バインディング (1) では、
INotifyPropertyChanged インターフェイスを実装した Person1 クラスに
プロパティを定義してデータ バインディングを構成しました。

ところで、dynamic 型として扱うことで実行時に任意のプロパティを追加できる ExpandoObject クラスというものがあり、
しかも INotifyPropertyChanged インターフェイスを実装しています。
したがって ExpandoObject クラスを使えば、
プロパティを事前に定義しなくてもデータ バインディングができるようになります。

次のコードは、C# のコードでデータ バインディング (1) の TwoWay バインディングのコードを少し書き換えて、
ExpandoObject クラスを使うようにしたものです。
Person1 クラスを使ったときと同様にデータ バインディングが動作します。

 

さて、ExpandoObject は、XAML で DataContext に設定した場合でもプロパティにアクセスできます。
次に示す XAML では、空の ExpandoObject オブジェクトに対して、
UI コントロールから Input プロパティと Output プロパティに直接バインドしています。

コード側で、Input を大文字に変換して Output に反映させています。

ExpandoBinding

 

前回: C# のコードでデータ バインディング (3)
次回: プロパティ変更をタイマーで同期して通知する

作成したサンプル
BindingConsole (GitHub)
ExpandoBindingWpf (GitHub)

バージョン情報
C# 6.0
.NET Framework 4.5

参照
データ バインディングの概要
C# のコードでデータ バインディング (1)

C# のコードでデータ バインディング (3)

前回の C# のコードでデータ バインディング (2) では、インデクサーの変更通知について説明しました。
今回は、コレクションのデータ バインディングについてです。

コレクションのデータ バインディングをするには、
まず当然の条件として Binding Source がコレクションであることが必要です。
これがさらに INotifyCollectionChanged インターフェイスを実装していると、
コレクション内のアイテムの増減や変更を通知することができ、永続的な OneWay バインディングができます。
INotifyCollectionChanged インターフェイスを実装していない場合は OneTime バインディングとなります。

.NET Framework には、INotifyCollectionChanged インターフェイスを実装したコレクションとして
ObservableCollection<T> クラスが用意されています。
通常はこの ObservableCollection<T> を使えば十分でしょう。

また、プロパティのときの Binding クラスに相当する機能 (Source と Target をつなぐ役割) は、
たいていは ItemsControl などのコレクションを扱うコントロールに組み込まれています。

次のコードで、ItemsControl と ObservableCollection<T> を使った
データ バインディングの動作を確認できます。

ItemsControl.ItemsSource プロパティに Binding Source を設定するだけです。
ItemsControl.Items プロパティは ItemsSource に設定されたコレクションのビューとして機能するもので、
UI はこの Items プロパティの状態を元に構築されます。
Items プロパティを通して、フィルターやソートなどを設定できます。

 

前回: C# のコードでデータ バインディング (2)
次回: ExpandoObject を使ったデータ バインディング

作成したサンプル
BindingConsole (GitHub)

バージョン情報
C# 6.0
.NET Framework 4.5

参照
データ バインディングの概要

C# のコードでデータ バインディング (2)

前回の C# のコードでデータ バインディング (1) では、
プロパティの変更通知を利用したデータ バインディングのコードを示しました。
今回は、インデクサーの変更通知について説明します (使う機会は少ないかもしれませんが)。

インデクサーの場合は、PropertyChangedEventArgs の PropertyName に渡す値として
Binding.IndexerName という定数が定義されており、その値は "Item[]" です。
インデックスの値に関係なくこの値を利用することになるため、
どのインデックスに対して値が変更されたのかを通知することはできません。

今回は、PersonMap というクラスの内部に Dictionary<int, string> を持たせ、
それにアクセスするためにインデクサーを定義しています。

Main メソッドでは、前回の TwoWay バインディングのコードを少し書き換えています。
これで双方向に値が同期されていることが確認できます。

 

前回: C# のコードでデータ バインディング (1)
次回: C# のコードでデータ バインディング (3)

作成したサンプル
BindingConsole (GitHub)

バージョン情報
C# 6.0
.NET Framework 4.5

参照
データ バインディングの概要

C# のコードでデータ バインディング (1)

XAML 系の開発で利用するデータ バインディングの仕組みを理解しやすくするため、
XAML ではなく C# のコードで、かつ、なるべく少ないコードでデータ バインディングを実装する例を示します。

まず、データ バインディングは 2 つのオブジェクトのプロパティの値を同期するための仕組みで、
その 2 つのオブジェクトのうち、主に一次的なデータとなるほうを Binding Source、
主に UI などで二次的に反映されるほうを Binding Target と呼びます。
そして 2 つのプロパティを関連付けるために、
System.Windows.Data.Binding オブジェクト (WPF の場合) を使います。

Binding には Mode プロパティ (BindingMode 列挙体) があり、どちらの方向に値を反映させるかを指定します。
次の種類があります。実際には、OneWay か TwoWay を使うことが多いでしょう。

  • OneTime: 初期化時の 1 回のみ、Source を Target に反映
  • OneWay: Source を Target に反映
  • OneWayToSource: Target を Source に反映
  • TwoWay: 双方向に反映
  • Default: Binding Target のプロパティのメタデータで定義された既定のモード (BindsTwoWayByDefault プロパティ)
    例えば、TextBlock.Text では OneWay、TextBox.Text では TwoWay です。

次に、Binding Target と Binding Source の満たすべき条件です。

  • Binding Target
    • DependencyObject (プロパティは DependencyProperty)
  • Binding Source (任意のオブジェクトだが、次のいずれかに該当する)
    • INotifyPropertyChanged
    • DependencyObject (プロパティは DependencyProperty)
    • その他の任意のオブジェクト
      プロパティ変更通知ができないため、Target への反映は初期化時の 1 回のみです。
      ただし、実際には特殊な動作をします。→ C# のコードでデータ バインディング (4) 

DependencyObject クラスは主に UI で使われるものであり、
データ モデルには通常 INotifyPropertyChanged インターフェイスを実装させます。

 

さて、ここからコードを書いていきますが、
プラットフォームは .NET のコンソール アプリケーションとし、
WPF のライブラリを参照して使うことにします。
したがって、Visual Studio でプロジェクトを作成したら、次のアセンブリを参照に追加します。

  • PresentationCore
  • PresentationFramework
  • System.Xaml
  • WindowsBase

まず、Binding Source となる Person クラスを定義します。
全部で 3 種類あり、Person0 はプレーンなクラス (POCO)、
Person1 は INotifyPropertyChanged インターフェイスを実装したクラス、
Person2 は DependencyObject クラスを継承したクラスとしました。

 

では、Main メソッドで OneWay のバインディングを実装してみます。
Person1.Name を TextBlock.Text に反映するだけの例です。
なお、TextBlock などの UI コントロールは UI スレッドでなければインスタンス化できないため、
Main メソッドに STAThread 属性を付けておきます。

データ バインディングを設定するには、
FrameworkElement.SetBinding メソッドまたは BindingOperations.SetBinding メソッドを呼び出します。

Person0 を使うとプロパティ変更通知が効かないため、最後の変更が反映されません。
Person2 の場合は、Person1 と同様に変更が反映されます。

次に、TwoWay のバインディングも実装してみます。
ここでは TextBlock の代わりに TextBox を使っています。

TextBox.Text は既定で TwoWay のため、Binding.Mode で明示的に指定しなくても TwoWay になります。
これで、Person1.Name と TextBox.Text の値が双方向に同期されます。

もちろん TextBlock を使った場合でも、

Mode = BindingMode.TwoWay

を指定すれば双方向で変更が反映されます。

 

次回: C# のコードでデータ バインディング (2)

作成したサンプル
BindingConsole (GitHub)

バージョン情報
C# 6.0
.NET Framework 4.5

参照
データ バインディングの概要
方法 : コードでバインディングを作成する
プロパティ変更とエラー情報の通知 (実装編)

データ バインディングと非同期処理 (その 2)

前回のデータ バインディングと非同期処理 (その 1) からの続きです。

前回は、UI スレッドで重い処理を実行してフリーズしてしまうアプリを作成しました。
今回は、非同期処理を使ってフリーズしないアプリに変更する方法について記述します。

 

■ TAP メソッドが提供されている場合

まず、ライブラリなどでタスクベースの非同期パターン (TAP) のメソッドが提供されている場合を考えます。
TAP メソッドが実装されているクラスの例としては、HttpClient クラスなどが挙げられます。
今回の例では、ConvertUtility クラスに ToUpperAsync メソッドが追加されたとします。

ConvertUtility.cs


public static class ConvertUtility

    // 同期メソッド。
    public static string ToUpper(string text)
    {
        Thread.Sleep(3000);
        return text.ToUpper();
    } 

    // TAP メソッド。
    public static Task<string> ToUpperAsync(string text)
    {
        return Task.Run(() => ToUpper(text));
    }
}


 

この場合は対応が非常に簡単です。
TextModel クラスのコンストラクターの中で、以前は

AddPropertyChangedHandler("Input", () => Output = ConvertUtility.ToUpper(Input));

としていたところを、次のように変更します。

AddPropertyChangedHandler("Input", async () => Output = await ConvertUtility.ToUpperAsync(Input));

async/await キーワードと、メソッド名の Async が追加されただけです。
このようにすると、時間のかかる処理はバックグラウンド スレッドで実行され、完了すると UI スレッドに戻ってきます。

実行してみると、アプリはすぐに起動し、下側の TextBlock には非同期的に値が反映されるようになります。

実行結果

 

■ 非同期メソッドが提供されていない場合

上の例の ToUpperAsync のようなメソッドが提供されておらず、ToUpper メソッドを使わざるを得ないような場合でも、
Task.Run メソッドを使えば対応できます。

TextModel クラスのコンストラクターの処理を次のように変更します。

AddPropertyChangedHandler("Input",
    async () => Output = await Task.Run(() => ConvertUtility.ToUpper(Input)));

Task.Run メソッドに渡した処理が完了すると UI スレッドに戻ってきます。

 

⋄ async/await を使わない方法

次のようなコードに変更しても、アプリはフリーズせずに動作します。

AddPropertyChangedHandler("Input",
    () => Task.Run(() => Output = ConvertUtility.ToUpper(Input)));

これだと Output プロパティにはバックグラウンド スレッドで値が設定されるため、
普通に考えると、UI 要素にアクセスできずに例外が発生してしまうように見えます。

しかし実は、Binding オブジェクトはデータソースの値を UI スレッドで取得します。
つまり Output プロパティの値を取得するときに UI スレッドに移るため、問題なく動作します。

 

■ EAP メソッドが提供されている場合

さらに、イベントベースの非同期パターン (EAP) の場合も考えてみましょう。
おそらく、現実にはまだ EAP にしか対応していないライブラリも少なくないと思います。

ConvertUtility クラスに対する EAP の実装として、次のクラスを作成します。

ConvertClient.cs


public class ConvertClient
{
    public event EventHandler<string> ToUpperCompleted = (o, e) => { };

    // EAP メソッド。
    public void ToUpperAsync(string text)
    {
        Task.Run(() => ConvertUtility.ToUpper(text))
            .ContinueWith(t => ToUpperCompleted(this, t.Result));
    }
}


 

TextModel クラスのコンストラクターの処理を次のように変更します。

var client = new ConvertClient();
client.ToUpperCompleted += (o, e) => Output = e;
AddPropertyChangedHandler("Input", () => client.ToUpperAsync(Input));

ToUpperCompleted イベントのイベント ハンドラーはバックグラウンド スレッドで実行されますが、
上述の通り、UI 側は Output プロパティの値を UI スレッドで取得します。

今回のイベント ハンドラーはバックグラウンド スレッドで実行されていますが、
既存のライブラリの中には、イベント ハンドラーが UI スレッドで実行されるものもあります。
いずれの場合でも、上記のコードで対応できます。

 

■ 注意点

  • 非同期処理は同時に複数実行されることもあるため、場合により同時実行制御 (lock など) が必要となります。
  • ここでは説明しませんでしたが、非同期プログラミング モデル (APM) の場合も同様に対応できると思います。

 

バージョン情報
C# 5.0
.NET Framework 4.5

参照
非同期プログラミングのパターン
Async/Await – 非同期プログラミングのベスト プラクティス (MSDN マガジン)
.NETで非同期ライブラリを正しく実装する
async/awaitと同時実行制御