WPFでReactiveProperty入門 ~アプリケーションのステータスやエラー情報をIObservable で通知する

この記事は、WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る - pierre3のブログ の続編です。

関連記事

  1. WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る - pierre3のブログ
  2. WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る (1. 検索バーの実装) - pierre3のブログ
  3. WPFでReactiveProperty入門 ~ Rxを使って検索結果のサムネイル画像を一括ダウンロードする - pierre3のブログ

(番外編)

サンプルプロジェクト github.com

目次

アプリケーションのステータスやエラーの情報の通知と記録

ReactiveBingViewer(この記事のサンプルアプリ)では、アプリケーション内のステータス情報の通知と記録を以下の通りに行います。

  • ステータスバー
    アプリケーションの現在の状況(「検索中...」や「検索が完了しました」などのメッセージ)をWindow下部のステータスバーに表示します。

f:id:pierre3:20151102111719p:plain

  • エラー通知パネル
    アプリケーション実行中の例外を補足した場合、以下のような通知パネルに例外の内容を表示します。 この通知パネルは、表示する例外メッセージが存在する場合のみ表示されます。右上の[×]ボタンをクリックするとパネル内のメッセージがクリアされてパネル自体も非表示となります。

f:id:pierre3:20151030212912p:plain

  • Logファイルへの記録
    上記通知を含め、アプリケーション内で発生したイベントはLog情報としてファイルへ記録します。

f:id:pierre3:20151029213955p:plain

ModelからViewModelへ ~ IObservable<LogMessage> による通知

LogMessageクラス

Model側で発生したステータスおよびエラー情報(以降まとめてLog情報と呼びます)は、LogMessage オブジェクトに格納されてIObservable<LogMessage> に乗ってViewModelへ通知されます。

LogMessage の定義は以下の通りです。

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クラス

実際にLog情報の通知を行う、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 を参照ください。

  • ILoggerインターフェースを実装します。Model側は、このインターフェースのメソッドを使用して通知を行います。
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 に変換

ViewModelでは、LogMessageNotifier を用途に合わせて以下の様に使用します。

  • ステータス情報(StatusMessage プロパティ、ステータスバーに表示)
    LogMessageNotifier を、 LogLevel.Info のメッセージのみを通すReactiveProperty に変換します
  • エラー情報 (ErrorLogs プロパティ。エラー通知パネルに表示)
    LogMessageNotifier を、 LogLevel.Warn 以上のメッセージを保持する ReactiveCollection に変換します
  • Log情報 (Logファイルに出力)
    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);
    }
}

エラー通知パネルのクリアと表示切替

エラー通知メッセージのクリアは、専用のReactiveCommand を実装して行います。

ReactiveCollection 内のアイテムをクリアする場合、IObservableから ReactiveCollection への変換を行うToReactiveCollection() または ToReadonlyReactiveCollection()の引数に ReactiveCommand.ToUnit() を渡すだけでクリア処理を実装することが出来ます。

//エラー通知クリア用コマンド
ClearErrorLogsCommand = new ReactiveCommand();
//Warn レベル以上のLogメッセージをReactiveCollectionに格納する
ErrorLogs = logger
    .Where(x => x.Level >= LogLevel.Warn)
    .ToReadOnlyReactiveCollection(ClearErrorLogsCommand.ToUnit());

また、エラー通知パネルに表示するメッセージが無い場合に、通知パネル自体を非表示とするために、以下の処理を実装しています。

  • エラー通知用コレクションのCollectionChangedイベントをIObservableで取得
  • Select()メソッドで、コレクションのアイテム数が1以上なら System.Windows.VIsibility.VIsible を、0ならSystem.Windows.Visibility.Collapsed を返すように変換する
  • これを ReactivePropertyに変換して、View側の通知パネルにバインドする
//エラーリスト表示切替。 ErrorLogs にアイテムがある場合のみ表示する
ErrorLogsVisibility = ErrorLogs
    .CollectionChangedAsObservable()
    .Select(_ => (ErrorLogs.Count > 0) ? Visibility.Visible : Visibility.Collapsed)
    .ToReactiveProperty(Visibility.Collapsed);

View の定義

該当部分のXAMLを以下に示します。

<!-- ステータスバー -->
<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">&#xE10A;</Button>
</Grid>

ソースコード

今回の記事に関連するソースコードの一覧です。
処理の詳細は、こちらをご参照ください。

まとめ

Notifier による通知と、ReactiveProperty を組み合わせることで、Modelから通知されるLog情報を、目的に合わせて柔軟かつスッキリと記述できるようになったと思います。