WPFでReactiveProperty入門 ~アプリケーションのステータスやエラー情報をIObservable で通知する
この記事は、WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る - pierre3のブログ の続編です。
関連記事
- WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る - pierre3のブログ
- WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る (1. 検索バーの実装) - pierre3のブログ
- WPFでReactiveProperty入門 ~ Rxを使って検索結果のサムネイル画像を一括ダウンロードする - pierre3のブログ
(番外編)
- WPFで「UriSouceプロパティに画像のURLを入れてBitmapImageを初期化する処理」を非同期で実行するとしぬ - pierre3のブログ
- [WPF] BitmapImage の生成・初期化を非同期で行う際のメモリの問題について - pierre3のブログ
サンプルプロジェクト github.com
目次 ReactiveBingViewer(この記事のサンプルアプリ)では、アプリケーション内のステータス情報の通知と記録を以下の通りに行います。
Model側で発生したステータスおよびエラー情報(以降まとめてLog情報と呼びます)は、LogMessage オブジェクトに格納されて LogMessage の定義は以下の通りです。 実際にLog情報の通知を行う、 ViewModelでは、LogMessageNotifier を用途に合わせて以下の様に使用します。 エラー通知メッセージのクリアは、専用のReactiveCommand を実装して行います。 ReactiveCollection 内のアイテムをクリアする場合、IObservableから ReactiveCollection への変換を行う また、エラー通知パネルに表示するメッセージが無い場合に、通知パネル自体を非表示とするために、以下の処理を実装しています。 該当部分のXAMLを以下に示します。 今回の記事に関連するソースコードの一覧です。 Notifier による通知と、ReactiveProperty を組み合わせることで、Modelから通知されるLog情報を、目的に合わせて柔軟かつスッキリと記述できるようになったと思います。
アプリケーションのステータスやエラーの情報の通知と記録
アプリケーションの現在の状況(「検索中...」や「検索が完了しました」などのメッセージ)をWindow下部のステータスバーに表示します。
アプリケーション実行中の例外を補足した場合、以下のような通知パネルに例外の内容を表示します。
この通知パネルは、表示する例外メッセージが存在する場合のみ表示されます。右上の[×]ボタンをクリックするとパネル内のメッセージがクリアされてパネル自体も非表示となります。
上記通知を含め、アプリケーション内で発生したイベントはLog情報としてファイルへ記録します。ModelからViewModelへ ~
IObservable<LogMessage>
による通知LogMessageクラス
IObservable<LogMessage>
に乗ってViewModelへ通知されます。public class LogMessage
{
//作成時刻
public DateTime CreatedAt { get; }
//メッセージ
public string Message { get; }
//Logレベル(Trace < Debug < Info < Warn < Error < Fatal )
public LogLevel Level { get; }
//例外オブジェクト
public Exception Exception { get; }
//例外オブジェクトの有無
public bool HasError { get { return Exception != null; } }
public LogMessage(string message, LogLevel level, Exception e = null)
{
CreatedAt = DateTime.Now;
Message = message;
Level = level;
Exception = e;
}
public override string ToString()
{
return (HasError)?
string.Format("{0}:[{1}] {2} [{3}]", Level.Name, CreatedAt.ToString("G"), Message, Exception.Message) :
string.Format("{0}:[{1}] {2}", Level.Name, CreatedAt.ToString("G"), Message);
}
}
LogMessageNotifierクラス
LogMessageNotifier
クラスを定義します
ScheduledNotifier<T>
を継承して、IObservable<T>
による通知を行いますScheduledNotifier<T>
は Reactive.Bingings.Notifiers 名前空間に含まれるReactiveProperty のクラスの1つで、IObservable<T>
による値の通知を行います。ScheduledNotifier<T>
を含む Notifier系クラスの詳細は ReactivePropertyのNotifier系クラス in C# for Visual Studio 2013 を参照ください。
public class LogMessageNotifier : ScheduledNotifier<LogMessage>, ILogger
{
public LogMessageNotifier(IScheduler scheduler) : base(scheduler) { }
public LogMessageNotifier() : base() { }
public void Trace(string message) => Report(LogMessage.CreateTraceLog(message));
public void Debug(string message) => Report(LogMessage.CreateDebugLog(message));
public void Info(string message) => Report(LogMessage.CreateInfoLog(message));
public void Warn(string message) => Report(LogMessage.CreateWarnLog(message));
public void Error(string message) => Report(LogMessage.CreateErrorLog(message));
public void Fatal(string message) => Report(LogMessage.CreateFatalLog(message));
public void Trace(string message, Exception e) => Report(LogMessage.CreateTraceLog(message, e));
public void Debug(string message, Exception e) => Report(LogMessage.CreateDebugLog(message, e));
public void Info(string message, Exception e) => Report(LogMessage.CreateInfoLog(message, e));
public void Warn(string message, Exception e) => Report(LogMessage.CreateWarnLog(message, e));
public void Error(string message, Exception e) => Report(LogMessage.CreateErrorLog(message, e));
public void Fatal(string message, Exception e) => Report(LogMessage.CreateFatalLog(message, e));}
}
public interface ILogger
{
void Trace(string message);
void Debug(string message);
void Info(string message);
void Warn(string message);
void Error(string message);
void Fatal(string message);
void Trace(string message,Exception e);
void Debug(string message, Exception e);
void Info(string message, Exception e);
void Warn(string message, Exception e);
void Error(string message, Exception e);
void Fatal(string message, Exception e);
}
public class MainWindowViewModel : IDisposable
{
private LogMessageNotifier logger = new LogMessageNotifier();
private WebImageStore webImageStore;
//...
public MainWindowViewModel()
{
//ModelはILoggerインターフェースを受け付ける
webImageStore = new WebImageStore(logger);
}
}
ViewModelからViewへ ~
IObservable<LogMessage>
をReactiveProperty に変換
LogMessageNotifier を、 LogLevel.Info のメッセージのみを通すReactiveProperty に変換します
LogMessageNotifier を、 LogLevel.Warn 以上のメッセージを保持する ReactiveCollection に変換します
LogMessageNotifier を購読(Subscribe) して、OnNext()でLogファイルへの書き込みを行いますpublic class MainWindowViewModel : IDisposable
{
private CompositeDisposable disposables = new CompositeDisposable();
private LogMessageNotifier logger = new LogMessageNotifier();
private WebImageStore webImageStore;
// ステータスバー・メッセージ
public ReadOnlyReactiveProperty<LogMessage> StatusMessage { get; private set; }
// エラー・メッセージ
public ReadOnlyReactiveCollection<LogMessage> ErrorLogs { get; private set; }
// エラー表示状態
public ReactiveProperty<Visibility> ErrorLogsVisibility { get; private set; }
// エラー表示クリア用コマンド
public ReactiveCommand ClearErrorLogsCommand { get; private set; }
public MainWindowViewModel()
{
//WebImageStoreのコンストラクタにLogMessageNotifierのインスタンスを渡す
webImageStore = new WebImageStore(logger);
//Logファイルの設定。今回はTextWriterTraceListenerで書き込む
System.Diagnostics.Trace.Listeners.Add(
new System.Diagnostics.TextWriterTraceListener("log.txt"));
//Logファイルに書き込み
logger.Subscribe(log =>
{
System.Diagnostics.Trace.WriteLine(log);
System.Diagnostics.Trace.Flush();
}).AddTo(disposables);
//Infoレベルのログはステータスバーに表示
StatusMessage = logger.Where(x => x.Level == LogLevel.Info)
.Select(x => x.Message)
.ToReadOnlyReactiveProperty();
//エラー通知クリア用コマンド
ClearErrorLogsCommand = new ReactiveCommand();
//Warn レベル以上のLogメッセージをReactiveCollectionに格納する
ErrorLogs = logger
.Where(x => x.Level >= LogLevel.Warn)
.ToReadOnlyReactiveCollection(ClearErrorLogsCommand.ToUnit());
//エラーリスト表示切替。 ErrorLogs にアイテムがある場合のみ表示する
ErrorLogsVisibility = ErrorLogs
.CollectionChangedAsObservable()
.Select(_ => (ErrorLogs.Count > 0) ? Visibility.Visible : Visibility.Collapsed)
.ToReactiveProperty(Visibility.Collapsed);
}
}
エラー通知パネルのクリアと表示切替
ToReactiveCollection()
または ToReadonlyReactiveCollection()
の引数に ReactiveCommand.ToUnit()
を渡すだけでクリア処理を実装することが出来ます。//エラー通知クリア用コマンド
ClearErrorLogsCommand = new ReactiveCommand();
//Warn レベル以上のLogメッセージをReactiveCollectionに格納する
ErrorLogs = logger
.Where(x => x.Level >= LogLevel.Warn)
.ToReadOnlyReactiveCollection(ClearErrorLogsCommand.ToUnit());
//エラーリスト表示切替。 ErrorLogs にアイテムがある場合のみ表示する
ErrorLogsVisibility = ErrorLogs
.CollectionChangedAsObservable()
.Select(_ => (ErrorLogs.Count > 0) ? Visibility.Visible : Visibility.Collapsed)
.ToReactiveProperty(Visibility.Collapsed);
View の定義
<!-- ステータスバー -->
<StatusBar DockPanel.Dock="Bottom">
<StatusBarItem DockPanel.Dock="Right">
<!-- プログレスパー (省略) -->
</StatusBarItem>
<Separator DockPanel.Dock="Right" />
<StatusBarItem>
<!-- ステータスメッセージ -->
<TextBlock Text="{Binding StatusMessage.Value,Mode=OneWay}"/>
</StatusBarItem>
</StatusBar>
<!-- エラー通知 -->
<Grid DockPanel.Dock="Bottom" Margin="4" Visibility="{Binding ErrorLogsVisibility.Value}">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Rectangle Grid.ColumnSpan="2" Stroke="Chocolate" Fill="LightYellow"/>
<!-- エラーメッセージ一覧 -->
<ListBox Grid.Row="3" ItemsSource="{Binding ErrorLogs}" MaxHeight="120" Margin="0,0,4,0"
Background="Transparent" BorderBrush="{x:Null}"/>
<Button Grid.Column="1" Margin="2" VerticalAlignment="Top" HorizontalAlignment="Right"
Command="{Binding ClearErrorLogsCommand}" Background="Transparent"
FontFamily="Segoe UI Symbol"></Button>
</Grid>
ソースコード
処理の詳細は、こちらをご参照ください。
まとめ