XAML へのシリアライズ (その 2)

前回の XAML へのシリアライズ (その 1) からの続きです。

前回は、ツリー状の構造になっているデータの集合を XAML にシリアライズする方法について記述しました。
つまり、あるオブジェクトの参照元はただ 1 つの親要素という前提で進めてきました。
しかし実は、XAML では任意の位置から他のオブジェクトを参照できます。
今回はその一例として、XAML をオブジェクト指向のデータストアとして利用する方法について考えます。

SQL Server をはじめとするリレーショナル データベースを利用する場合、
アプリケーション側で定義された .NET などのクラス、すなわちオブジェクト データモデルを、
リレーショナル データモデルに変換する必要がありました。

例えば、多対多の関連や配列をリレーショナル データベースに保存するには、便宜的にテーブルを追加するなどの措置が必要となります。
それに対してオブジェクト指向データベースの場合は、オブジェクト データモデルの形式のままでデータを保存できます。

 

では、オブジェクト指向のデータストアとして利用する具体例を示していきます。
概念データモデルが次の図で表されるとします。
これには、1 対多、多対多、継承の関係が含まれています。

概念データモデル

 

前回に示した方法で、この概念データモデルをクラスとして定義していきます。
ADO.NET Entity Framework の Code First でデータモデルを定義するのと同じ感覚で、次のようにできます。

UsersData.cs


using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Windows.Markup;

namespace XamlClassLib
{
    public class UsersData
    {
        Collection<Country> countries = new Collection<Country>();
        public Collection<Country> Countries { get { return countries; } }
        Collection<Group> groups = new Collection<Group>();
        public Collection<Group> Groups { get { return groups; } }
        Collection<User> users = new Collection<User>();
        public Collection<User> Users { get { return users; } }
    }

    [DebuggerDisplay(@"\{{Name}\}")]
    [ContentProperty("Users")]
    public class Country
    {
        public string Name { get; set; }
        Collection<User> users = new Collection<User>();
        public Collection<User> Users { get { return users; } }
    }

    [DebuggerDisplay(@"\{{Name}\}")]
    [ContentProperty("Users")]
    public class Group
    {
        public string Name { get; set; }
        Collection<User> users = new Collection<User>();
        public Collection<User> Users { get { return users; } }
    }

    [DebuggerDisplay(@"\{{GetType().Name}: {Name}\}")]
    [ContentProperty("Groups")]
    public class User
    {
        public string Name { get; set; }
        public Country Country { get; set; }
        Collection<Group> groups = new Collection<Group>();
        public Collection<Group> Groups { get { return groups; } }
    }

    public class SpecialUser : User
    {
        [DefaultValue(typeof(decimal), "0")]
        public decimal Discount { get; set; }
    }

    public class TrialUser : User
    {
        public DateTime Expiration { get; set; }
    }
}


 

さて、実際のデータが次のオブジェクト図で表されるとします。

オブジェクト図

 

コンソール アプリケーション プロジェクトでこれらのオブジェクトを構築して、XAML にシリアライズします。

Program.cs


using System;
using System.Collections.Generic;
using System.Linq;
using System.Xaml;
using XamlClassLib;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var japan = new Country { Name = "Japan" };
            var us = new Country { Name = "US" };
            var admins = new Group { Name = "Admins" };
            var customers = new Group { Name = "Customers" };
            var taro = new User { Name = "Taro" };
            var jiro = new SpecialUser { Name = "Jiro", Discount = 0.3m };
            var hanako = new TrialUser { Name = "Hanako", Expiration = new DateTime(2014, 1, 1) };

            taro.Country = japan;
            japan.Users.Add(taro);
            jiro.Country = us;
            us.Users.Add(jiro);
            hanako.Country = japan;
            japan.Users.Add(hanako);

            taro.Groups.Add(admins);
            admins.Users.Add(taro);
            taro.Groups.Add(customers);
            customers.Users.Add(taro);
            jiro.Groups.Add(customers);
            customers.Users.Add(jiro);

            var data = new UsersData();
            data.Countries.Add(japan);
            data.Countries.Add(us);
            data.Groups.Add(admins);
            data.Groups.Add(customers);
            data.Users.Add(taro);
            data.Users.Add(jiro);
            data.Users.Add(hanako);

            XamlServices.Save("UsersData.xaml", data);
        }
    }
}


 

すると、次の XAML ファイルが出力されます。

UsersData.xaml


<UsersData xmlns="http://schemas.saka-pon.net/xamlsample" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <UsersData.Countries>
    <Country x:Name="__ReferenceID3" Name="Japan">
      <User Country="{x:Reference __ReferenceID3}" x:Name="__ReferenceID0" Name="Taro">
        <Group x:Name="__ReferenceID5" Name="Admins">
          <x:Reference>__ReferenceID0</x:Reference>
        </Group>
        <Group x:Name="__ReferenceID1" Name="Customers">
          <x:Reference>__ReferenceID0</x:Reference>
          <SpecialUser x:Name="__ReferenceID2" Discount="0.3" Name="Jiro">
            <SpecialUser.Country>
              <Country x:Name="__ReferenceID4" Name="US">
                <x:Reference>__ReferenceID2</x:Reference>
              </Country>
            </SpecialUser.Country>
            <x:Reference>__ReferenceID1</x:Reference>
          </SpecialUser>
        </Group>
      </User>
      <TrialUser Country="{x:Reference __ReferenceID3}" x:Name="__ReferenceID6" Expiration="2014-01-01" Name="Hanako" />
    </Country>
    <x:Reference>__ReferenceID4</x:Reference>
  </UsersData.Countries>
  <UsersData.Groups>
    <x:Reference>__ReferenceID5</x:Reference>
    <x:Reference>__ReferenceID1</x:Reference>
  </UsersData.Groups>
  <UsersData.Users>
    <x:Reference>__ReferenceID0</x:Reference>
    <x:Reference>__ReferenceID2</x:Reference>
    <x:Reference>__ReferenceID6</x:Reference>
  </UsersData.Users>
</UsersData>


 

x:Name でオブジェクトに名前を付け、x:Reference でオブジェクトを参照していることがわかります。
もちろん、XamlServices.Load メソッドでデシリアライズすれば元のオブジェクトに戻ります。

上記の XAML ではオブジェクト ツリーが再帰的に出力されているため可読性は高くないですが、これは下記に示す XAML と同等です。
デシリアライズすると同一のオブジェクトが得られます。


<UsersData xmlns="http://schemas.saka-pon.net/xamlsample" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <UsersData.Countries>
        <Country x:Name="Japan" Name="Japan">
            <x:Reference>Taro</x:Reference>
            <x:Reference>Hanako</x:Reference>
        </Country>
        <Country x:Name="US" Name="US">
            <x:Reference>Jiro</x:Reference>
        </Country>
    </UsersData.Countries>
    <UsersData.Groups>
        <Group x:Name="Admins" Name="Admins">
            <x:Reference>Taro</x:Reference>
        </Group>
        <Group x:Name="Customers" Name="Customers">
            <x:Reference>Taro</x:Reference>
            <x:Reference>Jiro</x:Reference>
        </Group>
    </UsersData.Groups>
    <UsersData.Users>
        <User x:Name="Taro" Name="Taro" Country="{x:Reference Japan}">
            <x:Reference>Admins</x:Reference>
            <x:Reference>Customers</x:Reference>
        </User>
        <SpecialUser x:Name="Jiro" Name="Jiro" Country="{x:Reference US}" Discount="0.3">
            <x:Reference>Customers</x:Reference>
        </SpecialUser>
        <TrialUser x:Name="Hanako" Name="Hanako" Country="{x:Reference Japan}" Expiration="2014-01-01" />
    </UsersData.Users>
</UsersData>


この形式であれば Program.cs でのオブジェクト操作に対応しており、
オブジェクト指向データベースの構造としてイメージしやすいでしょう。

 

バージョン情報
.NET Framework 4.5

広告
カテゴリー: .NET Framework. タグ: . Leave a Comment »

XAML へのシリアライズ (その 1)

WPF をはじめとするユーザー インターフェイスを記述するための言語として紹介されることの多い XAML ですが、
XAML 自体は WPF などに依存しているわけではなく、例えば WF のアクティビティを記述するために利用されたり、
さらには設定情報や簡易的なデータを保存しておくためのストアとして利用されたりすることもあります。

この記事では、オブジェクトを XAML にシリアライズするために必要な情報をまとめていきます。
今回はまず、ツリー型のデータ構造を持つオブジェクトを取り扱います。

 

■ シリアライズの例

先に具体例を示します。
プロパティの定義や属性の指定の注意点については後ほど解説します。

クラス ライブラリ プロジェクトを作成し、参照に System.Xaml.dll を追加します。
次のようにクラスを定義します。

Preference.cs


using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Windows.Markup;

namespace XamlClassLib
{
    // コレクションをコンテンツとするクラス。
    [DebuggerDisplay(@"\{{Name}\}")]
    [ContentProperty("Cities")]
    public class Preference
    {
        public string Name { get; set; }
        Collection<City> cities = new Collection<City>();
        public Collection<City> Cities { get { return cities; } }
    }

    // 単一オブジェクトをコンテンツとするクラス。
    [DebuggerDisplay(@"\{{Name}\}")]
    [ContentProperty("Mayor")]
    public class City
    {
        public string Name { get; set; }
        public Coordinate Coordinate { get; set; }
        public Person Mayor { get; set; }
        public Statistics Statistics { get; set; }
        public string[] Products { get; set; }
        Collection<Person> people = new Collection<Person>();
        public Collection<Person> People { get { return people; } }
    }

    // コンテンツが存在しないクラス。
    [DebuggerDisplay(@"\{{Name}\}")]
    public class Person
    {
        public string Name { get; set; }
        public DateTime Birthday { get; set; }
    }

    // 既定値を利用するクラス。
    public class Statistics
    {
        [DefaultValue(1)]
        public int Area { get; set; }
        [DefaultValue(false)]
        public bool HasAirport { get; set; }

        public Statistics()
        {
            Area = 1;
        }
    }

    // 型コンバーターを利用するクラス。
    [TypeConverter(typeof(ConventionalStringConverter<Coordinate>))]
    public class Coordinate
    {
        public double Latitude { get; set; }
        public double Longitude { get; set; }

        public static Coordinate Parse(string value)
        {
            var s = value.Split(‘,’);
            return new Coordinate { Latitude = double.Parse(s[0]), Longitude = double.Parse(s[1]), };
        }

        public override string ToString()
        {
            return string.Format("{0},{1}", Latitude, Longitude);
        }
    }

    // シリアライズには ToString メソッドを、デシリアライズには Parse メソッドを利用する型コンバーター。
    public class ConventionalStringConverter<T> : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            return sourceType == typeof(string);
        }

        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            var v = (string)value;
            var parse = typeof(T).GetMethod("Parse", BindingFlags.Static | BindingFlags.Public);
            if (parse == null) throw new InvalidOperationException(string.Format("{0} クラスに Parse メソッドが見つかりません。", typeof(T).Name));
            return parse.Invoke(null, new object[] { v });
        }

        public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
        {
            return context.Instance is T && destinationType == typeof(string);
        }

        public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
        {
            return value.ToString();
        }
    }
}


 

さらに、AssemblyInfo.cs に、次の属性を追加します。
このようにすると、XML 名前空間を定義できます。この定義は必須というわけではありません。
XML 名前空間については、XAML 上での XML 名前空間の使用を参照するとよいでしょう。

[assembly: XmlnsDefinition("http://schemas.saka-pon.net/xamlsample", "XamlClassLib")]
[assembly: XmlnsPrefix("http://schemas.saka-pon.net/xamlsample", "xs")]

 

次に、コンソール アプリケーション プロジェクトを作成します。
適当なデータでオブジェクトを構築して、XAML にシリアライズします。

Program.cs


using System;
using System.Collections.Generic;
using System.Linq;
using System.Xaml;
using XamlClassLib;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var chuo = new City
            {
                Name = "中央区",
                Coordinate = new Coordinate { Latitude = 35.7, Longitude = 139.8 },
                Mayor = new Person { Name = "月島さん", Birthday = new DateTime(1950, 1, 1) },
                Statistics = new Statistics { Area = 1, HasAirport = false },
                Products = new[] { "人形焼", "佃煮" },
            };
            chuo.People.Add(new Person { Name = "晴海さん" });
            chuo.People.Add(new Person { Name = "築地さん" });

            var ota = new City
            {
                Name = "大田区",
                Coordinate = new Coordinate { Latitude = 35.6, Longitude = 139.7 },
                Mayor = new Person { Name = "蒲田さん", Birthday = new DateTime(1950, 1, 1) },
                Statistics = new Statistics { Area = 2, HasAirport = true },
                Products = new string[0],
            };

            var tokyo = new Preference { Name = "東京都" };
            tokyo.Cities.Add(chuo);
            tokyo.Cities.Add(ota);

            XamlServices.Save("Tokyo.xaml", tokyo);
        }
    }
}


 

XamlServices.Save メソッドにより、次のようにシリアライズされます。
逆にこれを、XamlServices.Load メソッドでデシリアライズできます。

Tokyo.xaml


<Preference Name="東京都"
  xmlns="http://schemas.saka-pon.net/xamlsample"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <City Coordinate="35.7,139.8" Name="中央区">
    <City.People>
      <Person Birthday="0001-01-01" Name="晴海さん" />
      <Person Birthday="0001-01-01" Name="築地さん" />
    </City.People>
    <City.Products>
      <x:Array Type="x:String">
        <x:String>人形焼</x:String>
        <x:String>佃煮</x:String>
      </x:Array>
    </City.Products>
    <City.Statistics>
      <Statistics />
    </City.Statistics>
    <Person Birthday="1950-01-01" Name="月島さん" />
  </City>
  <City Products="{x:Array Type=x:String}" Coordinate="35.6,139.7" Name="大田区">
    <City.Statistics>
      <Statistics Area="2" HasAirport="True" />
    </City.Statistics>
    <Person Birthday="1950-01-01" Name="蒲田さん" />
  </City>
</Preference>


 

以下では、オブジェクトを XAML にシリアライズおよびデシリアライズする際の指針や注意点について解説します。

■ プロパティの定義

(1) 非コレクション型のプロパティ

コレクション型以外のプロパティ (配列型を含む) は、get と set の対で宣言します。
上記の例のような自動実装プロパティである必要はありません。

(2) コレクション型のプロパティ

コレクション型 (ICollection<T> インターフェイスを実装した型) のプロパティは、get のみを宣言します。
プロパティの型宣言は ICollection<T> でもかまいません。
例えば、次のようにします。

Collection<City> cities = new Collection<City>();
public Collection<City> Cities { get { return cities; } }

以下は注意点です。

  • { get; private set; } と宣言した場合、
    XAML ファイルをコンパイルすると、set にアクセスできないという理由で失敗してしまうことがあります。
  • { get; set; } と宣言した場合、
    オブジェクトをシリアライズすると、上記の例では <City> の親要素として
    <sco:Collection x:TypeArguments="City"> が出力されてしまうことがあります。

 

■ 属性の指定

(1) TypeConverter 属性

オブジェクトを XML 要素としてではなく、XML 属性として出力させることができます。
TypeConverter を継承したクラスを作成しておく必要があります。
上記の例の Coordinate クラスのように、文字列表現が容易なオブジェクトに対して適用すると効果的です。

ただし、場合により次の問題点があります。
TypeConverter の CanConvertFrom メソッドでは変換前の型は渡されますが、値は渡されないため、
本当に変換できるかどうかは ConvertFrom メソッドが実行されてからでないとわからないことがあります。

このような場合、ValueSerializer を継承したクラスを追加し、ValueSerializer 属性を追加します。
ValueSerializer 属性は単独で指定しても効果を発揮しないようで、TypeConverter 属性と併用する必要があります。

TypeConverter では文字列に変換されるとは限りません (XAML 以外の用途でも利用される) が、
ValueSerializer は XAML 上での文字列表現に特化されたコンバーターです。

(2) ContentProperty 属性

対象のクラスに、コンテンツを表すプロパティを指定します。
コンテンツとして指定されたプロパティは、直下の子要素としてシリアライズされます。
例えば、<Preference> の直下に、<Preference.Cities> のような要素を挟まずに <City> が出力されます。

(3) DefaultValue 属性

対象のプロパティに、既定値を指定します。
シリアライズの際、プロパティの値がこの既定値と等しい場合は XML 属性が出力されなくなります。

ただし、逆に XML 属性のないプロパティをデシリアライズする際には既定値を設定してくれません。
したがって、既定値が自動で設定されないプロパティ (依存関係プロパティでないなど) の場合、
コンストラクターで既定値を設定しなければなりません。

(4) DesignerSerializationVisibility 属性

確か Visual Studio 2008 の頃の環境では、コレクション型のプロパティに

[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]

が指定されていないと開発ツールでうまくシリアライズされなかった気がしますが、
現在の環境ではこの属性を指定しなくても問題なさそうに見えます。

 

次回の XAML へのシリアライズ (その 2) では、
オブジェクト参照を用いて、XAML をオブジェクト指向データストアとして利用する方法を紹介します。

バージョン情報
.NET Framework 4.5

参照
XAML の概要 (WPF)
XamlServices クラス
XAML 上での XML 名前空間の使用

カテゴリー: .NET Framework. タグ: . 1 Comment »

データ バインディングと非同期処理 (その 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と同時実行制御

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

WPF をはじめとする XAML UI テクノロジにおいて、
通信やストレージ I/O などの時間のかかる可能性のある処理を非同期で実行させる方法について考えます。
前提として、モデル層のデータはデータ バインディングによって UI 層に反映されるものとします。

 

まずは準備として、WPF アプリケーション プロジェクトを作成し、次のようなメソッドを作成します。
文字列を変換させるだけですが、疑似的に時間のかかるメソッドとするために 3 秒間ブロックさせています。

ConvertUtility.cs


public static class ConvertUtility
{
    // 処理に時間のかかるメソッド。
    public static string ToUpper(string text)
    {
        Thread.Sleep(3000);
        return text.ToUpper();
    } 
}


 

次に、モデルとなるクラスを作成します。
入力と出力を表すプロパティのみを定義します。

TextModel.cs


public class TextModel : INotifyPropertyChanged
{
    private string input;
    public string Input
    {
        get { return input; }
        set
        {
            if (input == value) return;
            input = value;
            NotifyPropertyChanged();
        }
    }

    private string output;
    public string Output
    {
        get { return output; }
        private set
        {
            if (output == value) return;
            output = value;
            NotifyPropertyChanged();
        }
    }

    public event PropertyChangedEventHandler PropertyChanged = (o, e) => { };

    public void NotifyPropertyChanged([CallerMemberName]string propertyName = "")
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    public void AddPropertyChangedHandler(string propertyName, Action action)
    {
        if (action == null) throw new ArgumentNullException("action");

        PropertyChanged += (o, e) =>
        {
            if (e.PropertyName == propertyName)
            {
                action();
            }
        };
    }

    public TextModel()
    {
        // Input プロパティの値が変更されたら、それを変換して Output プロパティに設定します。
        AddPropertyChangedHandler("Input", () => Output = ConvertUtility.ToUpper(Input));
    }
}


 

TextModel クラスのコンストラクターには、
Input プロパティの値が変更されたら、それを変換して Output プロパティに設定する、
というコードを追加しています。

最後に UI です。
Window の DataContext に TextModel オブジェクトを設定し、
上側の TextBox を Input に、下側の TextBlock を Output にそれぞれバインドします。

MainWindow.xaml

MainWindow.xaml

 

このアプリを実行してみると、起動は遅く、テキストを編集したときにフリーズしてしまいます。
実は、Visual Studio や Blend のデザイン画面上での編集中にも同様にフリーズします。

実行結果

これは、時間のかかる処理を UI スレッドで実行していることが原因です。
次回のデータ バインディングと非同期処理 (その 2) では、これを非同期処理に変更する方法について記述します。

 

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