プログラミング Windows 第6版 第7章 WPF編
この記事では、「プログラミング Windows 第6版」を使って WPF XAML の学習を支援することを目的にしています。この目的から、書籍と併せて読まれることをお勧めします。
第7章 非同期性
この章では、.NET Framewrok 4.5(C# 5.0、Visual Basic 11)で追加された新しい非同期プログラミング(async、await)を説明しています。この説明の中で、MessageDialog クラスを取り扱っていますが、このクラスは WinRT のみで使用できるものになっています。WPF XAML では同等の機能はなく、MessageBox クラス (Windows Forms でも MessageBox クラス)があるだけになります。従って、7 章のタイトルにある非同期性を同じコードで説明するために、独自の MessageDialog クラスを用意して説明することにします。独自の MessageDialog クラスについては、説明の中で具体例な説明を盛り込んでいきます。
7.1(P245) スレッドとユーザー インターフェース
本節では、UI スレッドと時間のかかる処理の問題を取り上げ、.NET Framework 4.0 から採用されたタスク(System.Threading.Tasks.Task) クラスを使うことで非同期性と並行処理がサポート(TAP)されていることを説明していますので、基本概念の理解として熟読してください。.NET Framework 3.5 までは、自分でスレッド オブジェクトを使用するか、スレッド プールを使用して非同期プログラミング(APM、または EAP)をするしかありませんでした。
7.2(P246) MessageDialog の使用
本節では、WinRT の MessageDialog クラスを使って非同期メソッドの説明をしています。すでに説明したように、WPF には MessageDialog クラスがありませんので、HowToAsync1 プロジェクト用に MessageDialog クラスを作成しました。作成した MessageDialog クラスの MessageDialog.xaml を示します。
<Window x:Class="HowToAsync1.MessageDialog"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
Title="MessageDialog" Height="180" Width="370"
WindowStyle='ToolWindow' ShowInTaskbar='False' >
<Grid Margin='10'>
<TextBlock x:Name='message' HorizontalAlignment='Center'
Text='メッセージ' />
<StackPanel x:Name='body' Orientation='Horizontal' Margin='0, 30,0,0'>
</StackPanel>
</Grid>
</Window>
Window クラスを利用して、WindowStyle を「ToolWindow」(Close ボタンのみの表示)にし、ShowInTaskbar を「False」に設定しています。また、指定されたメッセージを表示するために TextBlock を配置し、指定されたコマンド ボタンを配置するための StackPanel を配置しています。今度は、MessageDialog.xaml.cs の抜粋を示します。
public partial class MessageDialog : Window
{
const double WIDTH = 100;
const double HEIGHT = 30;
const double MARGIN = 5;
object color;
public MessageDialog()
{
InitializeComponent();
}
public MessageDialog(string title, string message, DialogButton[] buttons)
: this()
{
this.Topmost = true;
this.Title = title;
this.message.Text = message;
this.color = null;
// ボタンのコマンド オブジェクト
var command = new DelegateCommand( (parameter) =>
{
// 戻り値を準備する
color = parameter;
// ダイアログを閉じる
this.Close();
});
// ボタンを追加します
foreach (var b in buttons)
{
var button = new Button()
{
Content = b.Title,
Width = WIDTH,
Height = HEIGHT,
Margin = new Thickness(MARGIN),
Command = command,
CommandParameter = b.Id
};
this.body.Children.Add(button);
}
}
// 非同期メソッド
public Task<Object> ShowAsync()
{
var dispatcherOperation = Dispatcher.InvokeAsync(() =>
{
var ret = this.ShowDialog();
// 戻り値を返す
return this.color;
}, System.Windows.Threading.DispatcherPriority.Normal);
dispatcherOperation.Completed += (s, args) => {
// 戻る値を返すまで、Objectを延命させる
};
return dispatcherOperation.Task;
}
}
class DelegateCommand : ICommand
{
Action<object> execute;
Func<object, bool> canExecute;
// Event required by ICommand
public event EventHandler CanExecuteChanged;
public DelegateCommand(Action<object> execute, Func<object, bool> canExecute)
{
this.execute = execute;
this.canExecute = canExecute;
}
public DelegateCommand(Action<object> execute)
{
this.execute = execute;
this.canExecute = AlwaysCanExecute;
}
public bool CanExecute(object parameter)
{
return canExecute(parameter);
}
public void Execute(object parameter)
{
execute(parameter);
}
// Default CanExecute method
bool AlwaysCanExecute(object param)
{
return true;
}
}
コンストラクターに、3 つの引数を渡します。
- title:メッセージ ダイアログに表示するタイトル文字列を指定します。
- message:メッセージ ダイアログに表示するメッセージとなる文字列を指定します。
- buttons:これからコードを示しますが、コマンドのタイトルと Id を指定した DialogButton の配列を指定します。これが、メッセージ ダイアログに表示するコマンドとなります。
この引数を受け取ってから、タイトルとメッセージを設定してから、StatckPanel に Button コントロールを追加します。Button コントロールで使用するコマンドのために、DelegateCommand クラスを用意しています。コマンドは、押されたボタンに対するオブジェクト(Id)をフィールドに設定するようにしています。そして、ShowAsync メソッドによって、MessageDialog ウィンドウをモーダルで表示して、戻り値である color フィールドを返すようにしています。今度は、DialogButton.cs を示します。
namespace HowToAsync1
{
public class DialogButton
{
public DialogButton(string title, object color)
{
this.Title = title;
this.Id = color;
}
public string Title { get; set; }
public object Id { get; set; }
}
}
作成した MessageDialog クラスにの使い方を示します。
var buttons = new DialogButton[]
{
new DialogButton("Red", Colors.Red),
new DialogButton("Green", Colors.Green),
new DialogButton("Blue", Colors.Blue)
};
var m = new MessageDialog("Choose a color", "How To Async #1", buttons);
var messageTask = m.ShowAsync();
DialogButton 配列のインスタンスを作成してから、MessageButton クラスのインスタンスを作成した後に ShowAsync メソッドを呼び出すことで、「Task<object> 」 が返ります。WinRT の MessageDialog.ShowAsync が返すのは「IAsyncOperarion<IUICommand> 」型であり、この型は WinRT 固有になります。Task<T> も IAsyncOperation<T> と同様に、await で戻り値を待機することができますので、非同期メソッドとの使い方は同じとなります(作成した ShowAsync メソッドの実装方法方は本章の後半に説明がありますので、ここでは WinRT と同じように非同期メソッドとして使用できると考えていただければ、結構です)。
HowToAsync1 プロジェクトで、MessageDialog.ShowAsync メソッドの戻り値を受け取るには、戻り値に対する継続タスクを使用します。それでは、MainWindow.xaml.cs を示します。
public partial class MainWindow : Window
{
Color clr;
public MainWindow()
{
InitializeComponent();
}
private void OnButtonClick(object sender, RoutedEventArgs e)
{
var buttons = new DialogButton[]
{
new DialogButton("Red", Colors.Red),
new DialogButton("Green", Colors.Green),
new DialogButton("Blue", Colors.Blue)
};
var m = new MessageDialog("Choose a color", "How To Async #1", buttons);
var messageTask = m.ShowAsync();
messageTask.ContinueWith((task) =>
{
var ret = task.Result;
if (ret == null) return; // 戻り値がnullの場合
clr = (Color)ret;
Dispatcher.BeginInvoke(
new Action(() => { OnDispatcherRunAsyncCallback(); }),
null);
});
}
void OnDispatcherRunAsyncCallback()
{
// Set the background brush
contentGrid.Background = new SolidColorBrush(clr);
}
}
ContinueWith メソッドにより継続タスクを登録しています。継続タスクで、Task オブジェクトの Result プロパティを使って戻り値を取得してから、Color 構造体へキャストしてから、Dispacher オブジェクトを使って Grid の Background プロパティに設定しています。それでは、実行結果を示します。
今度は、MainWindow.xaml を示します。
<Window ... >
<Grid x:Name="contentGrid">
<Button Content="Show me a MessageDialog!"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Click="OnButtonClick" />
</Grid>
</Window>
モーダルで MessageDialog クラスが表示されて、ボタンをクリックすると MessageDialog クラスが閉じて、ウィンドウの背景色がクリックしたボタンの色に設定されるようになります。書籍には、なぜ Dispatcher オブジェクトを使用するかが記述されています。この理由は、ContinueWith メソッドで登録された継続タスク(書籍で説明している Completed コールバックと同等であると理解してください)が、UI スレッドで動作しないことが理由となります。こうした考え方は、WPF XAML と WinRT XAML は同じですので、書籍の説明を熟読してください。
7.2(P252) コールバックでのラムダ関数の使用
本節では、WinRT の IAsyncOperation の Completed コールバックの代わりにラムダ関数を使用した方法を説明しています。このため、前節で作成した MessageDialog クラスを使用して、TaskAwaiter 構造体を使用した OnCompleted メソッドを使用して同等の処理にした、HowToAsync2 プロジェクトの MainWindow.xaml.cs の抜粋を示します。
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void OnButtonClick(object sender, RoutedEventArgs e)
{
var buttons = new DialogButton[]
{
new DialogButton("Red", Colors.Red),
new DialogButton("Green", Colors.Green),
new DialogButton("Blue", Colors.Blue)
};
var m = new MessageDialog("Choose a color", "How To Async #2", buttons);
var messageTask = m.ShowAsync();
var messageAwaiter = messageTask.GetAwaiter();
messageAwaiter.OnCompleted(() =>
{
var ret = messageAwaiter.GetResult();
if (ret == null) return; // 戻り値がnullの場合
Color clr = (Color)ret;
contentGrid.Background = new SolidColorBrush(clr);
});
}
}
Task<object> 型である messageTask の GetAwaiter 拡張メソッドを呼び出すことで、TaskAwaiter 構造体のインスタンスを取得しています。そして、TaskAwiter の OnCompleted メソッドにラムダ関数を指定することで、Grid の Background を設定しています。前節や WinRT XAML と大きく違う点は、Dispatcher オブジェクトを使用していない点となります。TaskAwaiter の OnCompleted メソッドは、GetAwaiter メソッドを呼び出した UI スレッドで処理されるという特徴を持っています。ここまでの WPF XAML としての説明は、非同期メソッドの呼び出しを UI スレッドと異なる継続タスクとして表現する方法と TaskAwaiter 構造体を使ったタスクの完了イベントを使って表現する方法を示すことを意図しています。Dispatcher 自体の使い方は書籍と異なりますが、非同期メソッドを同じような用途で扱うことができると理解していただくことを目的にしています。
7.4(P253) await 演算子
本節では、.NET Framework 4.5(C# 5.0、Visual Basic 11)で追加された async/ await の利用法を説明しています。前節までで、非同期メソッドの基本となる利用方法を踏まえて、async/ await を使用するとコードがどのように変化するかという点に焦点を当てています。それでは、HowToAsync3 プロジェクトの MainWindow.xaml の抜粋を示します。
private async void OnButtonClick(object sender, RoutedEventArgs e)
{
var buttons = new DialogButton[]
{
new DialogButton("Red", Colors.Red),
new DialogButton("Green", Colors.Green),
new DialogButton("Blue", Colors.Blue)
};
var m = new MessageDialog("Choose a color", "How To Async #3", buttons);
var result = await m.ShowAsync();
if (result == null) return; // 戻り値がnullの場合
Color clr = (Color)result;
contentGrid.Background = new SolidColorBrush(clr);
}
await を使用するには、メソッド定義に「async」を付与します。そして、非同期メソッドの呼び出しに「await」を付与することで、同期的にコードを記述することができます。この機能により、Dispatcher オブジェクトを記述する必要がなくなりました。もちろん、機能的には前節までに説明した HowToAsync1 と HowTowAsync2 プロジェクトと同じになります。書籍では、どのような場合に async を使用できるかを説明していますので、書籍を熟読してください。
Visual Studio 2012/2013 のコード エディタは、非同期メソッド呼び出しに対して「await」を記述しないと警告の波線を表示します。この警告を表示させたくない場合は、非同期メソッドの戻り値を変数に代入してください。この方法が、前節までに使用しているコードになります。
7.5(P256) 非同期操作の取り消し
本節では、非同期メソッドの呼び出しをキャンセルする方法を説明しています。WinRT の非同期メソッドの大半は、呼び出しをキャンセルすることができるようになっています。この仕組みを使って、キャンセルする方法を具体例を使って説明しています。前節まででWPF XAML 用に作成した MessagDialog クラスは、キャンセルの仕組みを実装していませんので、ここではキャンセルできるようにした HowToCancelAsync プロジェクトの MessageDialog.xaml.cs を抜粋して示します。
public partial class MessageDialog : Window
{
const double WIDTH = 100;
const double HEIGHT = 30;
const double MARGIN = 5;
object color;
DispatcherOperation<Object> dispatcherOperation;
CancellationToken token;
DispatcherTimer timer = null;
public MessageDialog()
{
InitializeComponent();
}
public MessageDialog(string title, string message, DialogButton[] buttons)
: this()
{
this.Topmost = true;
this.Title = title;
this.message.Text = message;
this.color = null;
// ボタンのコマンド オブジェクト
var command = new DelegateCommand( (parameter) =>
{
// 戻り値を準備する
color = parameter;
// ダイアログを閉じる
this.Close();
});
// ボタンを追加します
foreach (var b in buttons)
{
var button = new Button()
{
Content = b.Title,
Width = WIDTH,
Height = HEIGHT,
Margin = new Thickness(MARGIN),
Command = command,
CommandParameter = b.Id
};
this.body.Children.Add(button);
}
this.Unloaded += MessageDialog_Unloaded;
}
void MessageDialog_Unloaded(object sender, RoutedEventArgs e)
{
if (this.timer != null)
{
this.timer.Stop();
this.timer.Tick -= timer_Tick;
}
}
// 非同期メソッド
public Task<Object> ShowAsync()
{
dispatcherOperation = Dispatcher.InvokeAsync(() =>
{
var ret = this.ShowDialog();
// 戻り値を返す
return this.color;
}, System.Windows.Threading.DispatcherPriority.Normal);
dispatcherOperation.Completed += (s, args) => {
// 戻る値を返すまで Objectを延命させる
};
return dispatcherOperation.Task;
}
// キャンセル可能な非同期メソッド
public Task<Object> ShowAsync(CancellationToken token)
{
// 操作のキャンセルを可能にします(1秒間隔で確認します)
this.token = token;
timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromSeconds(1);
timer.Tick += timer_Tick;
timer.Start();
return ShowAsync();
}
void timer_Tick(object sender, EventArgs e)
{
token.ThrowIfCancellationRequested();
}
public void Cancell()
{
dispatcherOperation.Abort();
this.Close();
}
}
キャンセル可能な ShowAsync メソッド(CancellationToken を引数)を用意して、DispatcherTimer を使ってキャンセル可能かどうかを確認するようにしています。この MessageDialog クラスで、DispatcherTimer を使ってキャンセルを実現している理由は、Window オブジェクトをモーダルで表示しているためです。それでは、非同期メソッドをキャンセルする HowToCancelAsync プロジェクトの MainWindow.xaml.cs の抜粋を示します。
public partial class MainWindow : Window
{
CancellationTokenSource tokenSource;
public MainWindow()
{
InitializeComponent();
}
private async void OnButtonClick(object sender, RoutedEventArgs e)
{
var buttons = new DialogButton[]
{
new DialogButton("Red", Colors.Red),
new DialogButton("Green", Colors.Green),
new DialogButton("Blue", Colors.Blue)
};
tokenSource = new CancellationTokenSource();
var message = new MessageDialog("Choose a color", "How To Cancel Async", buttons);
// Start a five-second timer
var timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromSeconds(5);
timer.Tick += OnTimerTick;
timer.Start();
var asyncOp = message.ShowAsync(tokenSource.Token);
object result = null;
try
{
result = await asyncOp;
}
catch (Exception)
{
// The exception in this case will be TaskCanceledException
message.Close();
}
timer.Stop();
if (result == null) return; // 戻り値がnullの場合
Color clr = (Color)result;
contentGrid.Background = new SolidColorBrush(clr);
}
void OnTimerTick(object sender, EventArgs e)
{
tokenSource.Cancel();
}
}
CancellationTokenSource のインスタンスを作成して、ShowAsync メソッドの引数に CancellationToken を渡しています。また、DispatcherTimer を利用して MessageDialog を表示してから 5 秒後に CancellationTokenSource を使って Cancel メソッドを呼び出すことで、MessageDialog クラスにキャンセルを通知しています。結果として、MessageDialog が TaskCanceledException という例外を通知しますので、MessageDialog の Close メソッド を呼び出して MessageDialog を閉じています。WinRT XAML では、MessageDialog クラスがキャンセルをサポートしているので、MessageDialog を閉じるような操作は必要ありません。ここで重要なことは、非同期メソッドをキャンセルする場合に、TaskCanceledException という例外が通知されるということです。
7.6(P258) ファイル I/O へのアプローチ
本節では、WinRT XAML では System.IO 名前空間の ファイル I/O 系の API が縮小されて、WinRT 専用としてファイル I/O 用の API が提供されているということを説明しています。WPF XAMLではというよりも、.NET Framework 4.5 では、System.IO 名前空間の API のいくつかに対して非同期メソッドの提供という機能拡張がされています。具体的には、非同期ファイル I/O のドキュメントを参照してください。この理由から、次節以降では、WinRT の ファイル I/O を System.IO 名前空間に追加された非同期ファイル I/O に移植して説明をしていきます。この関係で、WinRT 固有の機能になることが多いことから、一般的に考えられる手法で読み替えていきます。これは、必ずこのようにして下さいというようなものではなく、このように置き換えられると考えられるだけのことです。自分の環境に置き換えて、適時読み替えて読み進めてください。
7.61.(P259) アプリケーション ローカル ストレージ
本節では、WinRT に固有なアプリケーション ローカル ストレージという概念を説明しています。デスクトップのアプリには、同様の概念はありません。一般的には、アプリを配置した場所であったり、ユーザーのドキュメント フォルダーなどが同様の目的で使用されます。但し、アプリを配置した場所が「Program Files」などの場合は、ファイル システムに対して書き換えなどを行うためには管理者権限が必要になることから、ユーザープロファイルなどに独自のフォルダーを作成するなどの処理が必要になりますので、ご注意ください。
7.6.2(P259) ファイル ピッカー
WinRT では、ユーザーがファイル システムからファイルを選択したりするための共通操作としてファイル ピッカー を用意しています。この ファイル ピッカーの基本的な考え方を説明しています。WPF XAML などのデスクトップ アプリは、Windows のコモン ダイアログを使用することでファイル システムからファイルを選択したりするための共通操作を行うことができます。WPF XAML では、Microsoft.Win32 名前空間に OpenFileDialog クラスと SaveFileDialog クラスを用意しています。Windows Forms では、System.Windows.Forms 名前空間で OpenFileDialog、SaveFileDialog、FolderBrowserDialog、FontDialog などを用意しています。つまり、WPF XAML では Windows Forms より少ないコモン ダイアログしか用意していません。この理由から不足するダイアログを使用するためには、Windows Forms のコモン ダイアログを使用するか、サードパーティー製のダイアログを使用するか、独自のコモン ダイアログを作成する必要があります。特に Windows Forms の ColorDialog などが返す型は、そのままでは WPF XAML で使用できないので、型を自分で変換する必要がある点に注意してください。型を自分で変換したとしても、スクラッチでコモン ダイアログを作成するよりは工数が少なくなるはずですので、用途によって最適なものを選択するようにしてください。
7.6.3(P260) バルクアクセス
本節で説明している概念は、WinRT 固有のものになります。従って、ファイル情報やフォルダー情報を扱う場合は、System.IO 名前空間の API を使用するようにしてください。
7.7(P261) ファイル ピッカーとファイル I/O
本節では、ここまでに説明したファイル ピッカーやファイル I/O 系の API の使い方を PrimitivePad プロジェクトという、メモ帳に似たアプリを使って説明してます。また、WPF XAML への基本的な考え方も説明してきていますので、PrimitivePad プロジェクトの MainWindow.xaml の抜粋を示します。
<Window ... >
<Window.Resources>
<Style x:Key="buttonStyle" TargetType="ButtonBase">
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="Margin" Value="0 12" />
</Style>
</Window.Resources>
<Grid Background="Black">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Content="Open..."
Grid.Row="0"
Grid.Column="0"
Style='{StaticResource buttonStyle}'
Click='OnFileOpenButtonClick' />
<Button Content='Save As...'
Grid.Row='0'
Grid.Column='1'
Style='{StaticResource buttonStyle}'
Click='OnFileSaveAsButtonClick' />
<ToggleButton x:Name='wrapButton'
Content='No Wrap'
Grid.Row='0'
Grid.Column='2'
Style='{StaticResource buttonStyle}'
Checked='OnWrapButtonChecked'
Unchecked='OnWrapButtonChecked' />
<TextBox x:Name='txtbox'
Grid.Row='1'
Grid.Column='0'
Grid.ColumnSpan='3'
FontSize='24'
ScrollViewer.HorizontalScrollBarVisibility='Auto'
ScrollViewer.VerticalScrollBarVisibility='Auto'
AcceptsReturn='True' />
</Grid>
</Window>
記述している XAML 自体は、すでに説明してきた変更だけで WPF XAML 用にしています。大きく異なるのは、各ボタンに対応するイベント ハンドラーの実装になります。最初に、 ファイルを開くボタンのイベント ハンドラーである OnFileOpenButtonClick を示します。
private async void OnFileOpenButtonClick(object sender, RoutedEventArgs e)
{
var dlg = new Microsoft.Win32.OpenFileDialog();
dlg.Filter = "テキスト(*.txt)|*.txt";
dlg.DefaultExt = ".txt";
var dlgResult = dlg.ShowDialog();
if (dlgResult != true)
{
return;
}
using (var stream = File.OpenRead(dlg.FileName))
{
var textReader = (TextReader) new StreamReader(stream);
var text = await textReader.ReadToEndAsync();
txtbox.Text = text;
}
}
FileOpenPicker を OpenFileDialog へ書き換えて、StrageFile などの WinRT API を System.IO の API へと置き換えています。また、非同期メソッドに対応するために、TextReader クラスに追加された ReadToEndAsync 非同期メソッド呼び出しを使用するようにしています。このように適切に書き換えることで、WinRT の ファイル I/O API を WPF XAML へ移植することができます。
今度は、 名前を付けて保存するボタンのイベント ハンドラーである OnFileSaveAsButtonClick を示します。
private async void OnFileSaveAsButtonClick(object sender, RoutedEventArgs e)
{
var dlg = new Microsoft.Win32.SaveFileDialog();
dlg.Filter = "テキスト(*.txt)|*.txt";
dlg.DefaultExt = ".txt";
var dlgResult = dlg.ShowDialog();
if (dlgResult != true)
{
return;
}
using (var stream = File.OpenWrite(dlg.FileName))
{
var textWriter = (TextWriter)new StreamWriter(stream);
await textWriter.WriteAsync(txtbox.Text);
textWriter.Close();
}
}
基本的な書き換え方法は、先程と同じです。FileSavePicker を SaveFileDialog へ書き換えて、StorageFile を System.IO の API へと置き換えています。ここでも、TextWriter クラスに追加された WriteAsync 非同期メソッド呼び出しを使用するようにしています。
残りは、Wrap ボタンのロジックになりますので、MainWindow.xaml.cs の抜粋を示します。
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Loaded += (sender, args) =>
{
//if (appData.Values.ContainsKey("TextWrapping"))
// txtbox.TextWrapping = (TextWrapping)appData.Values["TextWrapping"];
txtbox.TextWrapping = Properties.Settings.Default.TextWrapping;
wrapButton.IsChecked = txtbox.TextWrapping == TextWrapping.Wrap;
wrapButton.Content = (bool)wrapButton.IsChecked ? "Wrap" : "No Wrap";
//txtbox.Focus(FocusState.Programmatic);
txtbox.Focus();
};
}
....
private void OnWrapButtonChecked(object sender, RoutedEventArgs e)
{
txtbox.TextWrapping = (bool)wrapButton.IsChecked ? TextWrapping.Wrap :
TextWrapping.NoWrap;
wrapButton.Content = (bool)wrapButton.IsChecked ? "Wrap" : "No Wrap";
//appData.Values["TextWrapping"] = (int)txtbox.TextWrapping;
Properties.Settings.Default.TextWrapping = (bool)wrapButton.IsChecked ? TextWrapping.Wrap : TextWrapping.NoWrap;
Properties.Settings.Default.Save();
}
}
残りの変更点は、ApplicationData という WinRT 固有の機能をアプリケーション設定ファイルを使用して読みだして、保存するように変更しています。こうすることで、TextWrpping の設定が保存されて、次に起動した時に読み込まれるようになります。それでは、実行結果を示します。
このように適切にコードを書き換えることで、WinRT XAML と同等のことが WPF XAMLでもできることがわかったことでしょう。ちなみにアプリケーション設定ファイルに対して保存した情報は、ユーザー プロファイルに保存して、次回以降の起動ではユーザー プロファイルに保存した情報が読み込まれようになりますので、実行ファイルと同じにフォルダーに保存したアプリケーション設定ファイルを直接書き換えていないことに注意してください。
7.8(P266) 例外処理
本節では、await が catch ブロックで使用できないことから、どのような例外処理が考えられるかを説明しています。ここで説明しているのは、1 つの考え方ですから、必要に応じて応用する必要があります。これらの考え方は、WinRT 固有のものではありませんので、async/await を使用する全てのケースに当てはまります。
7.9(P267) 非同期呼び出しの統合
本節では、ファイルを開くという非同期操作を含む処理を共通化するためにどうしたら良いかという観点で説明をしています。説明の中で、非同期メソッドを設計する方法に説明が及んでいます。ここでは、非同期メソッドを設計する上で考えるべき事項を説明します。最初に、非同期メソッドの戻り値は、次に示す型にする必要があります。
戻り値型 | 説明 |
Task<T> | await 演算子で実行を待機し、戻り値として T を返します。 |
Task | await 演算子で実行を待機し、戻り値はありません。 |
void | await 演算子は利用できない、非同期メソッドとなります。 |
非同期メソッドを設計する上で、待機できるようにするためには「Task<T>」か「Task」を戻り値として設計する必要があります。ですが、可能な限り「Task」を返すメソッドの設計を避けるようにしてください。この理由は、async / await を記述するとコンパイラーによって非同期メソッドを待機するための実装コードが自動生成されるのですが、コンパイラーの最適化によっては自分が記述した順序でコードが実行されるとは限らないからです。つまり、コンパイラーの最適化に左右されない非同期メソッドの呼び出しとは、await した次のステップで、戻り値をチェックしてから処理をすることになります。このことを意味する疑似コードを示します。
Task MyMethodAsync(...)
{
// 何かの処理を行う
return 戻り値;
}
...
async void MyMethodCall()
{
var result = await MyMethodAsync(...);
if (result)
{
// ここで継続処理を行う
}
}
このように await 演算子で待機したメソッドの戻り値を、次のステップの if 文で処理しています。こうすることで、コンパイラーが最適化したとしても、「if (result) 」が必ず MyMethodAsync メソッドが結果を返してから実行されるようになります。このような考え方で、非同期メソッドを作成するのが望ましいと私は考えています。それでは、HowToAsync1 プロジェクト用に作成した MessageDialog クラスの非同期メソッドを再掲します。
// 非同期メソッド
public Task<Object> ShowAsync()
{
var dispatcherOperation = Dispatcher.InvokeAsync(() =>
{
var ret = this.ShowDialog();
// 戻り値を返す
return this.color;
}, System.Windows.Threading.DispatcherPriority.Normal);
dispatcherOperation.Completed += (s, args) => {
// 戻る値を返すまで、Objectを延命させる
};
return dispatcherOperation.Task;
}
この ShowAsync メソッドは、戻り値が「Task<Object>」になっており待機可能になっています。メソッドの中で行っていることを示します。
- Dispatcher オブジェクトの InvokeAsync メソッドを呼び出す。
InvokeAsync は、DispatcherOperartion<Object> 型を返す。
Window クラスの ShowDialog メソッド(モーダル)を呼び出してから、color フィールドを返す。 - DispatcherOperation の Completed イベントにハンドラーを登録する。
この処理は、コメントに記述したように ShowAsync メソッドが戻り値を安全に返すためだけに使用。 - ShowAsync メソッドの戻り値として、Task<Object> 型を返す。
await で待機するロジックでは、Object だけが戻ります。
このようにして、非同期メソッドを設計しています。参考までに、ShowAsync メソッドを呼び出している箇所をILDASM による逆アセンブラである IL を使って示します。
.method private hidebysig instance void OnButtonClick(object sender,
class [PresentationCore]System.Windows.RoutedEventArgs e) cil managed
{
.custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type) = ( 01 00 2A 48 6F 77 54 6F 41 73 79 6E 63 33 2E 4D // ..*HowToAsync3.M
61 69 6E 57 69 6E 64 6F 77 2B 3C 4F 6E 42 75 74 // ainWindow+<OnBut
74 6F 6E 43 6C 69 63 6B 3E 64 5F 5F 30 00 00 ) // tonClick>d__0..
.custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = ( 01 00 00 00 )
// コード サイズ 64 (0x40)
.maxstack 2
.locals init ([0] valuetype HowToAsync3.MainWindow/'<OnButtonClick>d__0' V_0,
[1] valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder V_1)
IL_0000: ldloca.s V_0
IL_0002: ldarg.0
IL_0003: stfld class HowToAsync3.MainWindow HowToAsync3.MainWindow/'<OnButtonClick>d__0'::'<>4__this'
IL_0008: ldloca.s V_0
IL_000a: ldarg.1
IL_000b: stfld object HowToAsync3.MainWindow/'<OnButtonClick>d__0'::sender
IL_0010: ldloca.s V_0
IL_0012: ldarg.2
IL_0013: stfld class [PresentationCore]System.Windows.RoutedEventArgs HowToAsync3.MainWindow/'<OnButtonClick>d__0'::e
IL_0018: ldloca.s V_0
IL_001a: call valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::Create()
IL_001f: stfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder HowToAsync3.MainWindow/'<OnButtonClick>d__0'::'<>t__builder'
IL_0024: ldloca.s V_0
IL_0026: ldc.i4.m1
IL_0027: stfld int32 HowToAsync3.MainWindow/'<OnButtonClick>d__0'::'<>1__state'
IL_002c: ldloca.s V_0
IL_002e: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder HowToAsync3.MainWindow/'<OnButtonClick>d__0'::'<>t__builder'
IL_0033: stloc.1
IL_0034: ldloca.s V_1
IL_0036: ldloca.s V_0
IL_0038: call instance void [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::Start<valuetype HowToAsync3.MainWindow/'<OnButtonClick>d__0'>(!!0&)
IL_003d: br.s IL_003f
IL_003f: ret
} // end of method MainWindow::OnButtonClick
簡単に説明するのであれば、<OnButtonClick>d__0' をローカル変数に保存してから AsyncMethodBuilder の Create メソッドを呼び出しています。ILDASM で確認すれば、<OnButtonClick>d__0 構造体が含まれていることがわかります。これらが、コンパイラーによって await 演算子によって生成されたものになります。<OnButtonClick>d__0 の中に、非同期呼び出しと待機後の値を使った処理が含まれています。そして重要なことは、async 演算子を使用したメソッドには非同期メソッドの呼び出し後のコードが含まれていないことです。このように自分で記述したコードを元に、コンパイラーが新しいコードを生成するのが async/await であり、コード生成の過程で最適化も行われるということになります。繰り返しますが、非同期メソッドを設計する場合は、コードの実行順序を制御しやすいように必ず Task<T> 型で設計するようにしましょう。
7.10(P270) ファイル I/O の最適化
本節では、WinRT でファイル I/O を扱う上でどのように扱うのが良いかということを説明しています。WPF XAML というよりも、デスクトップ用の .NET Framework にどのように対応させるのかという観点で説明すると、テキスト ファイルをまとめて読み書きするメソッドである ReadLines や WriteLines メソッドが System.IO.File クラスに .NET Framework 4.0 から追加されていますので、行単位などで処理するよりも効率化できるとも言えます。また、UI との関係で考えるのであれば、.NET Framework 4.5 以降で導入された 非同期ファイル I/O などの使用も検討した方がよいでしょう。
7.11(P271) アプリケーションのライフサイクルの問題
本節では、WinRT XAML のアプリ プロセスが影響を受けるプロセス ライフサイクルを説明しています。具体的には、未起動、実行中、一時停止という状態のことです。このようなプロセス状態は、Windows ストア アプリ固有のものになります。よって、デスクトップで動作する WPF XAML などには関係しません。デスクトップで動作するアプリの状態は、今までと同じで実行中、未起動の 2 つだけになります。もちろん、プロセスの正常終了や異常終了の可能性もあります。ですが、Windows ストア アプリでは、アプリ内からアプリのプロセスを終了することは推奨されていませんので、デスクトップ上のアプリと違って終了操作をアプリ側で用意しないことになります。
書籍に記述していることは、Windows ストア アプリ固有の課題に対するものであり、WPF XAML の世界には関係しません。が、ユーザー体験の考え方によっては前回の終了状態を次の起動時に再現した方が望ましい場合もあります。このようなユーザー体験を考える上では、本節に記載されていることも参考になることでしょう。
本節で使用している QuickNotes プロジェクトの MainWindow.xaml の抜粋を示します。
<Grid Background="Black">
<TextBox x:Name="txtbox"
AcceptsReturn="True"
TextWrapping="Wrap" />
</Grid>
この XAML は、組み込みのスタイルシートを除けば WinRT と同じになります。分離コードは、ファイル I/O 系の API が異なることから、書き換えています。それでは、MainWindow.xaml.cs の抜粋を示します。
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.Loaded += MainWindow_Loaded;
this.Closing += MainWindow_Closing;
}
async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
var folder = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
using (var stream = File.Open(System.IO.Path.Combine(folder, "QuickNotes.txt"), FileMode.OpenOrCreate))
{
var textReader = (TextReader)new StreamReader(stream);
txtbox.Text = await textReader.ReadToEndAsync();
txtbox.SelectionStart = txtbox.Text.Length;
}
//txtbox.Focus(FocusState.Programmatic);
txtbox.Focus();
}
async void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
var folder = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
using (var stream = File.Open(System.IO.Path.Combine(folder, "QuickNotes.txt"), FileMode.OpenOrCreate))
{
var textWriter = (TextWriter)new StreamWriter(stream);
await textWriter.WriteAsync(txtbox.Text);
textWriter.Close();
}
}
}
WinRT XAML と違って一時停止がないので、Window クラスの Closing イベントでデータを保存するようにしています。ユーザー 体験の観点では、タイマーなどで使って自動保存を用意しても良いかもしれません。どのように実装するかは、ユーザーが利用するシーンに合わせて意識することのない自然な方法を考えるのが良いでしょう。
7.12(P276) カスタム非同期メソッド
本節では、この記事ではすでに説明していますが、自分で非同期メソッドを設計する方法を説明しています。この記事では、WPF XAML に書籍の内容を適用する関係で、キャンセル可能な非同期メソッドの設計方法なども説明しました。考え方を書籍は説明していますので、この記事で非同期メソッドの作成方法を理解するのが難しいと感じた方は、書籍を熟読してください。
書籍では、非同期メソッドを作成する例として WordFreq プロジェクトを使用しています。それでは、WordFreq プロジェクトの MainWidnow.xaml.cs より GetWordFrequenciesAsync メソッドを示します。
Task<IOrderedEnumerable<KeyValuePair<string, int>>> GetWordFrequenciesAsync(
Stream stream,
CancellationToken cancellationToken,
IProgress<double> progress)
{
return Task.Run(async () =>
{
Dictionary<string, int> dictionary = new Dictionary<string, int>();
using (StreamReader streamReader = new StreamReader(stream))
{
// Read the first line
string line = await streamReader.ReadLineAsync();
while (line != null)
{
cancellationToken.ThrowIfCancellationRequested();
progress.Report(100.0 * stream.Position / stream.Length);
string[] words = line.Split(' ', ',', '.', ';', ':');
foreach (string word in words)
{
string charWord = word.ToLower();
while (charWord.Length > 0 && !Char.IsLetter(charWord[0]))
charWord = charWord.Substring(1);
while (charWord.Length > 0 &&
!Char.IsLetter(charWord[charWord.Length - 1]))
charWord = charWord.Substring(0, charWord.Length - 1);
if (charWord.Length == 0)
continue;
if (dictionary.ContainsKey(charWord))
dictionary[charWord] += 1;
else
dictionary.Add(charWord, 1);
}
line = await streamReader.ReadLineAsync();
}
}
// Return the dictionary sorted by Value (the word count)
return dictionary.OrderByDescending(i => i.Value);
}, cancellationToken);
}
このメソッドのコード自体は、WinRT XAML と同じになります。今度は、MainWindow.xaml の抜粋を示します。
<Grid>
<Grid HorizontalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button x:Name="startButton"
Content="Start"
Grid.Row="0" Grid.Column="0"
HorizontalAlignment="Center"
Margin="24 12"
Click="OnStartButtonClick" />
<Button x:Name="cancelButton"
Content="Cancel"
Grid.Row="0" Grid.Column="1"
IsEnabled="false"
HorizontalAlignment="Center"
Margin="24 12"
Click="OnCancelButtonClick" />
<ProgressBar x:Name="progressBar"
Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
Height="10"
Margin="24" />
<TextBlock x:Name="errorText"
Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2"
FontSize="24"
TextWrapping="Wrap" />
<ScrollViewer Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2">
<StackPanel x:Name="stackPanel" />
</ScrollViewer>
</Grid>
</Grid>
XAML 自体も今までと同じで組み込みスタイルを除けば、ProgressBar 要素に Height 属性を設定しただけになります。これは、WinRT XAML の ProgeressBar コントロールのデフォルトの高さとの違いによるものです。そして、MainWindow.xaml.cs の 基本的なコードも同じにすることができます。
public partial class MainWindow : Window
{
// Project Gutenberg ebook of Herman Melville's "Moby-Dick"
Uri uri = new Uri("https://www.gutenberg.org/ebooks/2701.txt.utf-8");
CancellationTokenSource cts;
public MainWindow()
{
InitializeComponent();
}
private async void OnStartButtonClick(object sender, RoutedEventArgs e)
{
...
}
private void OnCancelButtonClick(object sender, RoutedEventArgs e)
{
cts.Cancel();
}
void ProgressCallback(double progress)
{
progressBar.Value = progress;
}
Task<IOrderedEnumerable<KeyValuePair<string, int>>> GetWordFrequenciesAsync(
Stream stream,
CancellationToken cancellationToken,
IProgress<double> progress)
{
...
}
}
ここまでのコードは、WinRT XAML のものと同じになります。それでは、OnStartButtonClick のイベント ハンドラーのコードを示します。
private async void OnStartButtonClick(object sender, RoutedEventArgs e)
{
stackPanel.Children.Clear();
progressBar.Value = 0;
errorText.Text = "";
startButton.IsEnabled = false;
IOrderedEnumerable<KeyValuePair<string, int>> wordList = null;
try
{
var client = new HttpClient();
cancelButton.IsEnabled = true;
cts = new CancellationTokenSource();
var response = await client.GetAsync(uri, cts.Token);
using (var stream = await response.Content.ReadAsStreamAsync())
{
//cancelButton.IsEnabled = true;
//cts = new CancellationTokenSource();
wordList = await GetWordFrequenciesAsync(stream, cts.Token,
new Progress<double>(ProgressCallback));
cancelButton.IsEnabled = false;
}
}
catch (OperationCanceledException)
{
progressBar.Value = 0;
cancelButton.IsEnabled = false;
startButton.IsEnabled = true;
return;
}
catch (Exception exc)
{
progressBar.Value = 0;
cancelButton.IsEnabled = false;
startButton.IsEnabled = true;
errorText.Text = "Error: " + exc.Message;
return;
}
// Transfer the list of word and counts to the StackPanel
foreach (KeyValuePair<string, int> word in wordList)
{
if (word.Value > 1)
{
TextBlock txtblk = new TextBlock
{
FontSize = 24,
Text = word.Key + " \x2014 " + word.Value.ToString()
};
stackPanel.Children.Add(txtblk);
}
await Task.Yield();
}
startButton.IsEnabled = true;
}
WinRT XAMLでは、RandomAccessStreamReference クラスを使用していました。このクラスは、WinRT 固有であることから、System.Net.Http.HttpClient クラスを使用するように変更しています。この変更に伴って、CancellationTokenSource のインスタンスを作成する位置を、HttpClient の GetAsync メソッドの前に移動しています。この理由は、GetAsync メソッドがキャンセル可能になっているためです。後は、HttpResponseMessage クラスより Stream を取り出すことで、以降の処理は WinRT XAML と同じになっています。これで実行をしてみると、WinRT XAML と同じ結果を得ることができますが、1 点だけ異なる挙動を示すものがあります。それが、ProgressBar コントロールになります。実は、ProgressBar コントロールが GetWordFrequenciesAsync メソッドからコールバックで呼び出されているにも関わらず、進捗状況を表すように描画がなされません。この点が、WinRT XAML との大きな違いになります。
この理由は、WinRT XAML の描画メカニズム自体が、非同期メソッドに対する最適化が行われていることに対して、WPF XAML 環境は非同期メソッドとの最適化が行われていないためです。この問題を解決するには、数か所に手を加える必要があります。それでは、OnStartButtonClick と ProgressCallback を示します。
private void OnStartButtonClick(object sender, RoutedEventArgs e)
{
stackPanel.Children.Clear();
progressBar.Value = 0;
errorText.Text = "";
startButton.IsEnabled = false;
var startTask = new Task(() => StartTask());
startTask.Start();
}
void ProgressCallback(double progress)
{
// Backgroundに変更
Dispatcher.InvokeAsync(() =>
{
progressBar.Value = progress;
}, System.Windows.Threading.DispatcherPriority.Background);
//progressBar.Value = progress;
}
OnStartButtonClick では、StartTask メソッド 呼び出しの Task を作成して、Start メソッドで実行するだけにしました(非同期メソッドの呼び出しのみ)。そして、ProgressCallback では、Dispacher オブジェクトを使った更新に変更しています。それでは、追加した Start メソッドを示します。
async void StartTask()
{
IOrderedEnumerable<KeyValuePair<string, int>> wordList = null;
try
{
var client = new HttpClient();
await Dispatcher.InvokeAsync(() =>
{
cancelButton.IsEnabled = true;
}, System.Windows.Threading.DispatcherPriority.Background);
cts = new CancellationTokenSource();
var response = await client.GetAsync(uri, cts.Token);
using (var stream = await response.Content.ReadAsStreamAsync())
{
wordList = await GetWordFrequenciesAsync(stream, cts.Token,
new Progress<double>(ProgressCallback));
if (wordList != null || wordList.Count() == 0)
{
await Dispatcher.InvokeAsync(() =>
{
cancelButton.IsEnabled = false;
}, System.Windows.Threading.DispatcherPriority.Background);
}
}
}
catch (OperationCanceledException)
{
var t3 = Dispatcher.InvokeAsync(() =>
{
progressBar.Value = 0;
cancelButton.IsEnabled = false;
startButton.IsEnabled = true;
}, System.Windows.Threading.DispatcherPriority.Background);
return;
}
catch (Exception exc)
{
var t4 = Dispatcher.InvokeAsync(() =>
{
progressBar.Value = 0;
cancelButton.IsEnabled = false;
startButton.IsEnabled = true;
errorText.Text = "Error: " + exc.Message;
}, System.Windows.Threading.DispatcherPriority.Background);
return;
}
// Transfer the list of word and counts to the StackPanel
foreach (KeyValuePair<string, int> word in wordList)
{
if (word.Value > 1)
{
await Dispatcher.InvokeAsync(() =>
{
TextBlock txtblk = new TextBlock
{
FontSize = 24,
Text = word.Key + " \x2014 " + word.Value.ToString()
};
stackPanel.Children.Add(txtblk);
}, System.Windows.Threading.DispatcherPriority.Background);
}
await Task.Yield();
}
if (wordList != null)
{
await Dispatcher.InvokeAsync(() =>
{
startButton.IsEnabled = true;
}, System.Windows.Threading.DispatcherPriority.Normal);
}
}
追加した Start メソッドは、もともとの OnStartButtonClick イベント ハンドラーに記述しているコードとほとんどが同じです。異なるのは、Start メソッド自体が非同期メソッドであり、UI スレッドで実行されないことから、UI コントロールを操作する箇所を Dispacher オブジェクトを使用するように書き換えていることです。このようにすると、OnStartButtonClick イベント ハンドラーによって非同期メソッドが起動されるようになります。そうすると、非同期メソッドが UI スレッドとは異なるスレッド プールで実行を始めますので、Dispacher オブジェクトによって適切に UI コントロールの描画が行われるようになり、ProgressBar コントロールの描画の問題が解決します。それでは、実行結果を示します。
非同期メソッドの使用方法や設計方法などは、WinRT XAML と WPF XAML は同じになります。より正確に説明するのであれば、async/await は .NET Framework 4.5 以降の言語機能であり、WinRT などのランタイム側の機能ではないので同じなのは当たり前なのです。よって、書籍にかかれている内容は、WPF XAML にも通用するものになります。残念なのは、WPF が登場した時に async/await のような機能がなく、非同期プログラミングはプログラマーに任されていたことから、ランタイム レベルでの非同期プログラミングへの最適化が行われていないことです。この意味では、WinRT XAMLは、非同期プログラミングありきで登場しましたから、当然のこととして最適化が行われているのです。
このような違いを知っていれば、WPF XAML であっても async/await を使用することが問題になることはないでしょう。最適なユーザー体験を提供するためには、ユーザーの操作をブロックすることは最悪の手法になりますから、内容によっては積極的に非同期プログラミングを使うことになるでしょう。是非、ユーザーが望むような体験を作り込んでください。
Window のスタイルについて
本章では、非同期プログラミングを書籍に合わせて読み進めるために MessageDialog クラスを作成しました。実は、Windows Forms の Form クラスと WPF の Window クラスでは、設定できるスタイルに違いがあります。
Window クラスでスタイルを設定するには、WindowStyle プロパティに WindowStyle 列挙を設定します。具体的な設定例を示します。
デフォルトが、SingleBorderWindow であり、MessageDialog クラスでは ToolWindow を設定していました。一方で、Windows Forms の Form クラスでは、ControlBox、MinimizeBox、MaximizeBox、FormBorderStyle プロパティなどを使用します。この中で、大きな違いは ウィンドウの閉じるボタンを非表示にする手段が、Window クラスにない点になります。これは、WPF XAML は ウィンドウの閉じるボタンを隠すという見せ方を推奨していないことを意味します。ユーザーにとって、Windows OS の操作に慣れていると仮定すると、閉じるボタンがあるという前提で操作をするのが普通のことになります。従って、閉じるボタンを表示しておくのが一般的であるということになります。どうしても、閉じるボタンを非表示にしたい場合だけは、Win32 API を使用することになります。
また、Windows Forms がサポートする MDI ウィンドウに関しては、標準でサポートされていません。従って、MDI を WPF XAML で実現する場合は、自分で作成するか、サードパーティー製のコントロールを使用することを検討してください。
ここまで説明してた内容を意識しながら、第7章を読むことで WPF にも書籍の内容を応用することができるようになることでしょう。