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情報を、目的に合わせて柔軟かつスッキリと記述できるようになったと思います。

[WPF] BitmapImage の生成・初期化を非同期で行う際のメモリの問題について

この記事は、前回のエントリ WPFで「UriSouceプロパティに画像のURLを入れてBitmapImageを初期化する処理」を非同期で実行するとしぬ - pierre3のブログ
の続きになります。

目次

前回のおさらい

前回は、Web画像のURLからBitmapImageを、UIをブロックする事無く生成するには、以下の様に記述すると良さそう。
というところまで書きました。

public async Task<BitmapImage> DownloadImageAsync(string url)
{
    using (var web = new System.Net.Http.HttpClient())
    {
        //HttpClientでbyte配列に先読みしておいて
        var bytes = await web.GetByteArrayAsync(url).ConfigureAwait(false);
        // そのbyte配列でMemoryStream を作成し、StreamSourceプロパティに設定
        using (var stream = new MemoryStream(bytes))
        {
            var bitmap = new BitmapImage();
            bitmap.BeginInit();
            bitmap.StreamSource = stream;
            bitmap.CacheOption = BitmapCacheOption.OnLoad;
            bitmap.EndInit();
            bitmap.Freeze();
            return bitmap;
        }
    }
} 

しかし、このコードにはメモリリークを引き起こす問題が2つあります。
まずは、そのうちの1つから

MemoryStream はDisposeしても内部バッファを離さない

上のコードでは、EndInit()での初期化が終わった時点でStreamSourceプロパティに設定したMemoryStreamは不要になり、using のスコープを抜けるとDispose()されます。

しかし、MemoryStream では、コンストラクタに渡した byte配列の参照を内部バッファとして保持しているのですが、それをDispose()後も抱えたまま離さないようなのです。

f:id:pierre3:20151024135114p:plain

※ Dispose()後も_buffer が残ったまま!

画像データとしては、Bitmapにデコードされた表示用の画素データと、デコード前のbyte配列(内部バッファ)の両方を持つことになります。
この状態のBitmapImageは、単純計算で必要な量の2倍のメモリ空間を占有しているってことです。
特に大きな画像を読み込んだ場合は、なかなか無視できない容量になるのではないでしょうか。

Dispose時にMemoryStreamの参照を外すストリームクラスでラップする

対処法は、以下サイトにありました。

MemoryStreamをラップしたStreamクラスを作って Dispose時にMemoryStreamの参照を外すという方法でこの問題を回避するようです。

以下のようなStreamのラップクラスを用意して

/// <summary>
/// ストリームのWrapperクラス
/// </summary>
/// <remarks>
/// Dispose 時に、内部ストリームの参照を外します
/// </remarks>
public class WrappingStream : Stream
{
    Stream m_streamBase;

    public WrappingStream(Stream streamBase)
    {
        if (streamBase == null)
        {
            throw new ArgumentNullException("streamBase");
        }
        m_streamBase = streamBase; //渡したStreamを内部ストリームとして保持
    }

    //Streamクラスのメソッドをオーバーライドして、内部ストリームの同じメソッドをそのまま呼ぶだけ
    public override Task<int> ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken)
    {
        ThrowIfDisposed();
        return m_streamBase.ReadAsync(buffer, offset, count, cancellationToken);
    }
    public new Task<int> ReadAsync(byte[] buffer, int offset, int count)
    {
        ThrowIfDisposed();
        return m_streamBase.ReadAsync(buffer, offset, count);
    }
    //...(中略)...

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            m_streamBase.Dispose();
            m_streamBase = null;  //disposeしたら内部ストリームをnullにして参照を外す
        }
        base.Dispose(disposing);
    }
    private void ThrowIfDisposed()
    {
        if (m_streamBase == null)
        {
            throw new ObjectDisposedException(GetType().Name);
        }
    }
}

MemoryStream をこれでラップしたうえで StreamSourceプロパティに設定します。

public async Task<BitmapImage> DownloadImageAsync(string url)
{
    using (var web = new HttpClient())
    {
        var bytes = await web.GetByteArrayAsync(url).ConfigureAwait(false);
        using (var stream = new WrappingStream(new MemoryStream(bytes)))
        {
            var bitmap = new BitmapImage();
            bitmap.BeginInit();
            bitmap.StreamSource = stream;
            bitmap.CacheOption = BitmapCacheOption.OnLoad;
            bitmap.EndInit();
            bitmap.Freeze();
            return bitmap;
        }
    }
} 

以下、サムネイル画像50枚分のBitmapImageを作成した状態のスナップショットでヒープを比較したものです。効果は一目瞭然だと思います。

・MemoryStreamのみの場合 f:id:pierre3:20151024214006p:plain

・WrappingStreamを使用した場合 f:id:pierre3:20151024214020p:plain

これで問題の1つは解決できました。

そして、もう1つの問題が以下になります。

DispatcherObjectをバックグラウンドスレッドで作成するとメモリリークする

この問題については、以下のエントリに詳しいので、そちらをご参照ください。

上記エントリで記述されている通り、BeginInvokeShutdown()Dispatcher.Run を使って回避する方法もあるようなのですが、
今回はバックグラウンドスレッド上でBItmapImageを生成しないようにすることで、対処したいと思います。

ダウンロード処理とBitmapImage生成処理を分離する

最も時間のかかるダウンロード部分のみバックグラウンドで非同期実行して、ダウンロードした結果を基にBitmapImageを生成・初期化する部分はUIスレッド上で行う様にします。

画像データを非同期ダウンロードしてbyte配列で返す

まずは、画像データのダウンロード部分。

(2015/10/26 訂正)
System.Net.Http.HttpClient の GetByteArrayAsync() の結果をそのまま返すだけです。
System.Net.Http.HttpClient の GetByteArrayAsync() を await してその結果を返します。

// このままではDownload完了前に HttpClient がDispose()されてしまう
//public static Task<byte[]> DownLoadImageBytesAsync(string url)
//{
//    using (var web = new HttpClient())
//    {
//        return web.GetByteArrayAsync(url);
//    }
//}

public static async Task<byte[]> DownLoadImageBytesAsync(string url)
{
    using (var web = new HttpClient())
    {
        return await web.GetByteArrayAsync(url);
    }
}

受け取ったbyte配列からBitmapImageを作成する

上記メソッドで取得したbyte配列を受け取って、MemoryStream を作ります。
さらに、WrappingStreamにラップしたものをStreamSourceプロパティに設定してBitmapImageを初期化します。

public static BitmapImage CreateBitmap(byte[] bytes, bool freezing = true)
{
    using (var stream = new WrappingStream(new MemoryStream(bytes)))
    {
        var bitmap = new BitmapImage();
        bitmap.BeginInit();
        bitmap.StreamSource = stream;
        bitmap.CacheOption = BitmapCacheOption.OnLoad;
        bitmap.EndInit();
        if (freezing && bitmap.CanFreeze)
        { bitmap.Freeze(); }
        return bitmap;
    }
}

DownLoadImageBytesAsync()をバックグラウンドで実行後、指定したディスパッチャ上でCreateBitmap()を実行する

上の2つのメソッドを組み合わせて呼び出します。
CreateBitmap() を実行するDispatcher を引数で指定できるようにしています。
今回はUIスレッド上で処理を行いたいので、App.Current.Dispatcher を指定します。

public static async Task<BitmapImage> DownloadImageAsync(string url, 
    Dispatcher dispatcherForBitmapCreation)
{
    var bytes = await DownLoadImageBytesAsync(url).ConfigureAwait(false);
    return await dispatcherForBitmapCreation.InvokeAsync(() => CreateBitmap(bytes));
}

// 使用例
// var bitmap = await DownloadImageAsync(downloadUrl, App.Current.Dispatcher);

おまけ

Rxを使って一度にダウンロードする場合は、DownloadImageAsync() にまとめず、個別に実行する方が分かりやすいかもしれません。

private ObservableCollection imageSource;
public void DownloadSearchResult(IEnumerable<Uri> imageUrlList)
{
    //IEnumerable<T>を、スレッドプール上で動作するIObservable<T> に変換
    disposable = imageUrlList.ToObservable(ThreadPoolScheduler.Instance)
        //画像データをまとめてダウンロード
        .SelectMany(async uri => await DownLoadImageBytesAsync(uri))
        //UIスレッドに処理を移す
        .ObserveOn(Reactive.Bindings.UIDispatcherScheduler.Default)
        //BitmapImageを作成してコレクションに追加
        .Subscribe( bytes => imagesSource.Add(CreateBitmap(bytes));   
}

//ここで使用しているReactive.Bindings.UIDispatcherScheduler は
//ReactiveProperty が用意するクラスです。App.xaml.cs でAppクラスのOnStartup()あたりで
// Reactive.Bindings.UIDispatcherScheduler.Initialize();
//としておくと、UIDispatcherScheduler.Default で UIスレッドのDispatcherが取得できます

WPFで「UriSouceプロパティに画像のURLを入れてBitmapImageを初期化する処理」を非同期で実行するとしぬ

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

関連記事

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

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

目次

はじめに

今回は、検索結果として取得した画像のURLから画像データを読み込んでBitmapImageを生成する部分について書くのですが、 先に言ってしまうと、試してみたけど結果うまく行きませんでした。というお話です。

BitmapImageの初期化にUriSourceを使うとUIがブロックされる

BitmapImageにWeb上の画像を読み込ませるには、UriSourceプロパティを使用するのが最も簡単です。
UriSourceプロパティに画像のURLを指定するだけで、画像データのダウンロードとBitmapへのデコードをBitmapImage自身がやってくれます。

public BitmapImage CreateBitmapImage(Uri sourceUri)
{
    var bitmap = new BitmapImage();
    bitmap.BeginInit();
    bitmap.UriSource = sourceUri;
    bitmap.EndInit();
    return bitmap;
}
// 以下の様にコンストラクタにURIを渡すのと同じ
// var bitmap = new BitmapImage(sourceUri);

非常に簡単で便利なのですが、この処理はUIをブロックするようなのです。
実際に、上記のコードを使って複数の画像を連続してダウンロードすると、すべての画像が生成されるまでUIの表示が更新されず、操作もできない状態になります。
(ダウンロード完了を待たずにEndInit()を抜けて、DownloadCompletedイベントで完了通知を受け取るようになっているので、ダウンロード自体はバックグラウンドで行われているように見えるのですが)

非同期にしてみる

そこで、Taskにツッコんで別スレッドで実行するようにしてみます。

public Task<BitmapImage> CreateBitmapImageAsync(Uri sourceUri)
{
    return Task.Run(()=> {
        var bitmap = new BitmapImage();
        bitmap.BeginInit();
        bitmap.UriSource = sourceUri;
        bitmap.EndInit();
        return bitmap;
    }
}

しかし、上記コードではImageコントロール等で表示しようとした時点で以下のエラーが発生します。

型 'System.Windows.Markup.XamlParseException' のハンドルされていない例外が PresentationFramework.dll で発生しました
DependencySource は、DependencyObject と同じ Thread 上で作成する必要があります。

UIスレッド以外で生成されたUIオブジェクトはFreeze()してからでないとUIスレッドで使用できないのでした。

UIで使用する前に Freeze しておく必要があるのですが、そのタイミングも問題になります。
例えば、以下の様にEndInit() の後に Freeze しようとしても、この時点では画像のダウンロードが完了していないので、Freezeできません。

public Task<BitmapImage> CreateBitmapImageAsync(Uri sourceUri)
{
    return Task.Run(()=> {
        var bitmap = new BitmapImage();
        bitmap.BeginInit();
        bitmap.UriSource = sourceUri;
        bitmap.EndInit();
        bitmap.Freeze();  //初期化が終わっていないのでFreezeできません!!
        return bitmap;
    }
}

ダウンロード完了を待ってからFreezeする

ダウンロードの完了を待ってからFreezeすればどうでしょうか? 試してみます。
ダウンロードの完了は DownloadCompleted イベントで受け取れます。
また、ダウンロード中に発生した例外は、DownloadFailed イベントで補足できます。

private ObservableCollection images = new ObservableCollection();
private Task CreateBitmapImage(Uri sourceUri)
{
    return Task.Run(()=> {
        var bitmap = new BitmapImage();
        bitmap.BeginInit();
        bitmap.UriSource = sourceUri;
        bitmap.CacheOption = BitmapCacheOption.OnLoad;
        bitmap.DownloadCompleted += (sender,args) => {
            var image =(BitmapImage)sender; 
            image.Freeze();        //Freezeしてから
            images.Add(image);     //Binding Sourceのコレクションに追加
        };
        bitmap.DownloadFailed += (sender,args) => {
          //失敗したらここに来る(はず)
          Trace.WriteLine($"DL Failed. [{args.ErrorException}]");  
        };
        bitmap.EndInit();
    }
}

UIスレッド以外で実行すると謎の沈黙

これでうまく行きそうな気がしますが、今度はまた別の問題が発生します。

  • DownloadCompleted が発火しない!
  • DownloadFailed にも飛んでこない!

しかも、例外で落ちることもなく沈黙する最悪のパターンです。

何か手掛かりは無いか、VisualStudio(2015)の診断ツールで「イベント」を確認すると、何やら謎の例外がスローされて直後にキャッチされているのが分かります。
f:id:pierre3:20151024090321p:plain

どうやら、System.Deployment.Application.InvalidDeploymentException なる例外が発生して、尚且つそれが握りつぶされているのが原因のようです。

このInvalidDeploymentExceptionってなんなんでしょう? 例外発生時の呼び出し履歴を見てみます。

System.Deployment.dll!System.Deployment.Application.ApplicationDeployment.CurrentDeployment.get()
System.Deployment.dll!System.Deployment.Application.ApplicationDeployment.IsNetworkDeployed.get()
PresentationCore.dll!MS.Internal.AppModel.SiteOfOriginContainer.SiteOfOriginForClickOnceApp.get()
PresentationCore.dll!MS.Internal.SecurityHelper.ExtractUriForClickOnceDeployedApp()
PresentationCore.dll!MS.Internal.SecurityHelper.BlockCrossDomainForHttpsApps(System.Uri uri = {不明})
PresentationCore.dll!System.Windows.Media.Imaging.BitmapDownload.BeginDownload(System.Windows.Media.Imaging.BitmapDecoder decoder = {不明}, System.Uri uri = {不明}, System.Net.Cache.RequestCachePolicy uriCachePolicy = {不明}, System.IO.Stream stream = {不明})
PresentationCore.dll!System.Windows.Media.Imaging.LateBoundBitmapDecoder..ctor(System.Uri baseUri = {不明}, System.Uri uri = {不明}, System.IO.Stream stream = {不明}, System.Windows.Media.Imaging.BitmapCreateOptions createOptions = {不明}, System.Windows.Media.Imaging.BitmapCacheOption cacheOption = {不明}, System.Net.Cache.RequestCachePolicy requestCachePolicy = {不明})
PresentationCore.dll!System.Windows.Media.Imaging.BitmapDecoder.CreateFromUriOrStream(System.Uri baseUri = {不明}, System.Uri uri = {不明}, System.IO.Stream stream = {不明}, System.Windows.Media.Imaging.BitmapCreateOptions createOptions = {不明}, System.Windows.Media.Imaging.BitmapCacheOption cacheOption = {不明}, System.Net.Cache.RequestCachePolicy uriCachePolicy = {不明}, bool insertInDecoderCache = {不明})
PresentationCore.dll!System.Windows.Media.Imaging.BitmapImage.FinalizeCreation()
PresentationCore.dll!System.Windows.Media.Imaging.BitmapImage.EndInit()

EndInig()メソッドの内部でLateBoundBitmapDecoder が生成されて、そのコンストラクタ内でBitmapDownload.BeginDownload() が実行され、 SecurityHelper.BlockCrossDomainForHttpsApps() メソッド内でClickOnce Deployed がなんちゃら・・・・
うーん、よくわかりません。
これ以上の深追いはやめておきます。

UriSourceを使ったBitmapImageの初期化はUIスレッドで

色々試してみましたが、今回のようなアプローチの仕方ではうまく行きそうにありません。

「UriSource を使った一連の処理は、UIスレッド以外で実行しないようにしましょう。」

別のアプローチ

では、非同期で処理できないの?というわけではなく、別の方法があります。

以下の様に、画像データを先にダウンロードしておき、MemoryStream でBitmapImageに流してやればOKです。

public async Task<BitmapImage> DownloadImageAsync(string url)
{
    using (var web = new HttpClient())
    {
        var bytes = await web.GetByteArrayAsync(url).ConfigureAwait(false);
        using (var stream = new MemoryStream(bytes))
        {
            var bitmap = new BitmapImage();
            bitmap.BeginInit();
            bitmap.StreamSource = stream;
            bitmap.CacheOption = BitmapCacheOption.OnLoad;
            bitmap.EndInit();
            bitmap.Freeze();
            return bitmap;
        }
    }
} 

Web検索等で調べると、このようなやり方が一般的のようですね。
(だったら最初からそうしろってことですが、結果がダメでもいろいろ試してみることに意義があるという事で)

しかし、これにもまだ問題があります。 上のDownloadImageAsync()を実行するとメモリリークを引き起こすのですが、長くなってきたので続きは次回!

WPFでReactiveProperty入門 ~ Rxを使って検索結果のサムネイル画像を一括ダウンロードする

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

関連記事

  1. WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る - pierre3のブログ
  2. WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る (1. 検索バーの実装) - pierre3のブログ

サンプルプロジェクト

github.com

今回は、Bing画像検索を実行し、検索結果からサムネイル画像を取得して一覧表示する処理について、Model側に焦点を当てて見てみたいと思います。

目次

Modelの概要

今回は、Model側の処理がメインになりますので、Model側の主要クラスについて簡単にまとめておきます。

クラス 説明
Bing.BingSearchContainer Microsoftが提供するBing Search APIのプロキシクラス
Bing.ImageResult Bing Search APIが返す画像検索結果のクラス。検索結果の画像1枚の情報が格納されている
WebImage 検索結果の画像1枚を表すクラス。Bing.ImageResultに格納されている画像URLから画像データをダウンロード後、BitmapImageとして保持する
WebImageStore WebImageのコレクションを管理するクラス。画像検索を実行し、検索結果からWebImageを生成してコレクションに保持する
WebImageHelper WebImage、WebImageStoreクラスを補助するメソッドを提供するクラス

http://pierre3net.azurewebsites.net/Content/image/Models.svg

Bing Search API プロキシクラス

Bing Search API をAzure Market Placeから購入(無料含む)すると、AzureポータルからBing Searchのプロキシクラス(BingSearchContainer.csファイル)をダウンロードして使用することが出来ます。

今回はこれをそのまま使用させて頂きました。

BingSearchContainer

このプロキシクラスは、WCFデータサービスの仕組みを利用しており、System.Data.Services.Client.DataServiceContextを継承したBingSearchContainerクラスを用いて検索を行います。

画像検索の実行には、Image() メソッドを使用します。 引数で検索条件等を色々指定できるのですが、とりあえずは第1引数に検索ワードを入れたら後はnullでOKです。

※ パラメータの詳細は、下記リンクの"Optional Parameters" の項目で確認できます。 https://msdn.microsoft.com/en-us/library/dd250942.aspx

//Bing Image検索プロキシクラス
public class BingSearchContainer : System.Data.Services.Client.DataServiceContext
{
    //画像検索用のクエリを発行
    public DataServiceQuery<ImageResult> Image(String Query, String Options, 
        String Market, String Adult, Double? Latitude, Double? Longitude, String ImageFilters)
    {
        DataServiceQuery<ImageResult> query;
        query = base.CreateQuery<ImageResult>("Image");
        //...(中略)...
        return query;
    }
}

Image()メソッドは、画像検索用クエリを表す DataServiceQuery<ImageResult> を返却します。
DataServiceQuery<ImageResult>Execute()メソッドでクエリを実行すると、検索結果がIEnumerable<ImageResult>のかたちで返却されます。

var bing = new Bing.BingSearchContainer(new Uri("https://api.datamarket.azure.com/Bing/search/"));
bing.Credentials = new NetworkCredential("accountKey", accountKey);
//画像検索用のクエリ(DataServiceQuery<Bing.ImageResult>)を発行
var query = bing.Image(searchWord, null, null, null, null, null, null);
//検索実行
IEnumerable<T> result = query.Execute();

ImageResult

画像検索結果が格納されるImageResultクラスは以下のプロパティを持ちます

//画像検索の結果を格納するクラス
public class ImageResult
{
    public Guid ID { get; set; }
    public String Title { get; set; }
    public String MediaUrl { get; set; }
    public String SourceUrl { get; set; }
    public String DisplayUrl { get; set; }
    public Int32? Width { get; set; }
    public Int32? Height { get; set; }
    public Int64? FileSize { get; set; }
    public String ContentType { get; set; }
    public Thumbnail Thumbnail { get; set; }
}
public class Thumbnail
{
    public String MediaUrl { get; set; }
    public String ContentType { get; set; }
    public Int32? Width { get; set; }
    public Int32? Height { get; set; }
    public Int64? FileSize { get; set; }
}

MediaUrlに画像データをダウンロードするためのURLが格納されています。
また、画像サイズを縮小したサムネイル用の画像データを取得することが可能で、その場合はThumbnailプロパティのMediaUrlを使用します。

検索結果のシーケンスからサムネイル画像をまとめて非同期ダウンロードする。

IEnumerable<ImageResult> として返される全ての検索結果(最大50件)のサムネイル画像を一度にダウンロードするのですが、(当然ながら)UIをブロックしないよう非同期で行う必要があります。

Reactive Extensions で非同期ダウンロード

上記のような非同期操作の実装はいくつか考えられますが、今回はReactive Extensionsを使用した方法で実装を試してみたいと思います。

IEnumerable<ImageResult> から IObservable<ImageResult> に変換

IE から IO への変換は、ToObservable() 拡張メソッドを使用します。

今回のサンプルでは、検索ワードを渡すとBing画像検索を実行して結果をIObservable<ImageResult>で返すヘルパーメソッドWebImageHelperクラスに用意しています。

ここでは、DataServiceQuery<ImageResult>をExecute()して得られるIEnumerable<ImageResult>ToObservable()IObserbable<ImageResult> に変換たものをreturn しています。

static class WebImageHelper
{
    //Bing Image 検索を実行して 結果を IObservable<T> で返す
    public static IObservable<Bing.ImageResult> SearchImageAsObservable(
        string searchWord, string accountKey, int skip, int top)
    {
        var bing = new Bing.BingSearchContainer(
            new Uri("https://api.datamarket.azure.com/Bing/search/"));
        bing.Credentials = new NetworkCredential("accountKey", accountKey);
        var query = bing.Image(searchWord, null, null, null, null, null, null);
        query = query.AddQueryOption("$skip", skip);
        query = query.AddQueryOption("$top", top);
        try
        {
            IEnumerable<T> result = query.Execute();
            return result.ToObservable();
        }
        catch (Exception e)
        {
            return Observable.Throw<Bing.ImageResult>(e);
        }
    }
}

例外をIObserbable<T> に乗せるには Observable.Throw<T>()

検索実行中に発生した例外は、そのままでは IO<T> の一連の流れの中では補足できませんが、 Observable.Throw<T>() メソッドを使用することで、例外をIObservable<T> として扱うことが可能になります。

上記の例では、検索実行中に発生した例外は IObservable<ImageResult> で包まれて、購読側のOnError()メソッドで補足できるようになります。

SelectMeny()で非同期メソッドを実行する

Rxで流れてきた検索結果オブジェクトImageResult を基にサムネイル画像のダウンロードを行う処理は、以下のメソッドを用いて行います。

ここでは、ImageResult オブジェクトを引数にWebImageインスタンスを生成後そのDownLoadThumbnailAsync()メソッドでサムネイル画像のダウンロードを非同期に行います。戻り値はTask<WebImage> となります。

public class WebImageStore : IDisposable
{
    //検索結果オブジェクト(ImageResult)でWebImageのインスタンスを生成後、
    //ImageResultに格納されているサムネイル画像のURLから画像データをダウンロードする
    private async Task<WebImage> CreateWebImageAsync(Bing.ImageResult bingResult)
    {
        var image = new WebImage(bingResult, logger);
        await image.DownLoadThumbnailAsync();
        return image;
    }
}

Rxの中で非同期メソッドを実行するには、SelectMeny() を使用します。
SelectMeny()に渡すデリゲートをasync にして、その中で await CreateWebImageAsync() とします。

こうすることで、CreateWebImageAsync() メソッドは非同期かつ並列的に実行されます。実行の結果は、完了した順に後続へ渡ります。

この一連の処理をSubscribe()すると、OnNext()にサムネイルのダウンロードが完了したWebImageオブジェクトが渡ってくるので、これを ObservableCollection に逐次追加して行きます。

以下に、実装例を示します。(実際には進捗通知等の処理が入り、もう少し複雑になっていますが、ここでは説明のため簡略化しています)

public class WebImageStore : IDisposable
{
    private IDisposable disposable;
    private ObservableCollection<WebImage> imagesSource;
    public ReadOnlyObservableCollection<WebImage> readonlyImages;
    public IReadOnlyObservableCollection<WebImage> Images {get {return readonlyImages;}}

    public void DownloadSearchResult(string searchWord)
    {
        disposable = WebImageHelper.SearchImageAsObservable(searchWord, bingAccountKey)
            .SelectMany(async bingResult => await CreateWebImageAsync(bingResult))
            .Where(webImage => webImage?.Thumbnail != null)
            .Subscribe(
                onNext: webImage =>
                {
                    //ObservableCollection に追加
                    imagesSource.Add(webImage);
                },
                onError: e =>
                {
                    //SearchImageAsObservable()内でObservable.Throw()した例外はここで補足
                    logger.Error("画像の検索に失敗しました。", e);
                },
                onCompleted: () =>
                {
                    logger.Info("検索が完了しました。");
                });
    }
}

まとめ

これで、検索を実行して、結果をサムネイルの一覧で表示するための枠組みが出来ました。
次回は、URLからサムネイルの画像データをダウンロードする処理と、そのデータを元にBitmapImageオブジェクトを生成する処理の詳細について書きたいと思います。

(補足) Roslyn で C#のソースコードからPlantUMLのクラス図を生成する の設計メモ

前回の記事の補足資料です。

pierre3.hatenablog.com

目次

使い方

前回、PlantUmlClassDiagramGenerator の使い方を書くのを忘れていましたので載せておきます。

PlantUmlClassDiagramGenerator はコンソールアプリケーションです。以下のパラメータを受け付けます。

PlantUmlClassDiagramGenerator.exe SOURCE_PATH [DEST_DIR]

  • 第1引数: SOURCE_PATH
    • 変換元のソースファイル名または、ソースファイルを格納したフォルダ名を指定します。
    • フォルダを指定した場合は、直下にある .cs ファイルをすべて読み込みます。(フォルダの階層は辿りません)
  • 第2引数: DEST_DIR
    • 結果のPlantUMLファイルの出力先を指定します。省略可能です。
    • 省略した場合は、入力ファイルと同じ階層に"uml" というフォルダを作成してそこに出力します。
C:\ > PlantUmlClassDiagramGenerator.exe SOURCE_PATH DEST_DIR

設計メモ

C#からPlantUMLのクラス図に変換する処理の設計メモです。

  • UMLに関する知識不足等で、不適切な部分があるかもしれません。 お気づきの点などありましたらご指摘頂けると嬉しいです。

型定義

インターフェース、クラス、構造体などの型定義に関する仕様です。
C#のキーワードとPlantUMLでの記述との対応付けを表にしてみました。

型キーワード

PlantUMLでは、interface , class , abstract class , enum が使用可能です。

C# PlantUML Memo
class class
struct <<struct>> class PlantUMLにはstruct で定義できる型が無いので、class にステレオタイプ<<struct>> を付加することで構造体を表現
interface interface
abstract class abstract class 抽象クラスはabstract class で宣言可能
enum enum

修飾子

修飾子は、(基本的には)ステレオタイプで表現します。

C# PlantUML Memo
abstract abstract ‘abstract‘ は、‘class‘ キーワードと組み合わせて抽象クラスの宣言時に使用する
static <<static>>
partial <<partial>>
sealed <<sealed>>
  • 型のアクセス修飾子について
    UMLに型自体のアクセス修飾子に関する規定がない(?)ため、型に対するアクセス修飾子は無視するようにしました。
    必要な場合は、<<internal>> class ClassA の様にステレオタイプを付加する?

型引数

ジェネリックの型引数は、PlantUMLでもC#と同じように書けます

class GenericsType<string,int> { 
}

クラス定義の変換例です。

//csharp
sealed class ClassA{ }
abstract class AbstractClass{ }
static class StaticClass{ }
struct Structure{ }
enum EnumType{ }
'plantuml
@startuml
class ClassA <<sealed>>
abstract class AbstractClass
class StaticClass <<static>>
class Structure <<struct>>
enum EnumType
class GenericsType<string,int>
@enduml

http://pierre3net.azurewebsites.net/Content/image/types.svg


メンバー定義

フィールド、プロパティ、メソッドEnum定数 など型のメンバー定義に関する仕様です。

アクセス修飾子(共通)

C# PlantUML Comment
public +
internal <<internal>> ~ (package) が意味合い的に近いと思うが、 protected internal と合わせてステレオタイプにした
protected internal # <<internal>> #~ はPlantUMLではエラーになるので internal をステレオタイプで表現することに
protected #
private -

修飾子 (共通)

C# PlantUML Comment
abstract {abstract} ステレオタイプではなく{}で括る。イタリック体の表記になる
static {static} ステレオタイプではなく{}で括る。下線付きの文字で表現される
virtual <<virtual>>
override <<override>>
readonly <<readonly>>

プロパティ

プロパティの表現は悩みどころですが、変換のしやすさを優先して以下の様に出力するようにしました。

  • C#のコードに記述されているアクセサー(get,set)をそのままステレオタイプとして出力
//csharp
public int PropA {get; set;}
public int PropB {get;}
public int PropC {get; protected set;}
'prantuml
+ PropA : int <<get>> <<set>>
+ PropB : int <<get>>
+ PropC : int <<get>> <<protected set>>
  • 別候補

他にも以下のように変換することも考えたのですが
(1) は protected set 等、アクセサーごとにアクセス修飾子が付けられた際の表現が難しく、 (2) は 変換処理が少し複雑になるのと、もはやプロパティではなくなるので却下しました。

'prantuml

'(1) <<property>> ステレオタイプを付け、getterのみの場合はreadonlyの制約を付ける
+ PropA : int <<property>>
+ PropB : int <<property>> { readonly }

'(2) Java風にメソッドで表現
+ getPropA():int
+ setPropA(value:int)
+ getPropB():int
+ getPropC():int
# setPropC(value:int)

初期化子(フィールド、プロパティ)

フィールド、プロパティの初期値を初期化子で設定している場合、初期値がリテラルの場合のみ = (初期値) を付加するようにしています。

リテラルのみにした理由は、初期化子にコンストラクタメソッドを使った場合に、 PlantUMLが 初期化子の() を見て フィールド(プロパティ)ではなくメソッドと判断してしまう為です。

'plantuml
''次のようにフィールドを記述しても、PlantUMLはメソッドと判断してしまう
class ClassA{
    # int : IList<int> = new List<int>()
}

初期化子の変換例

//csharp
class ClassA 
{
    private readonly int intField = 100;
    protected double X = 0, Y = 1, Z = 2;
    internal double PropC { get; } = 3.141592;
    protected IList<int> list = new List<int>();
}
'plantuml
class ClassA {
    - <<readonly>> intField : int = 100
    # X : double = 0
    # Y : double = 1
    # Z : double = 2
    <<internal>> PropC : double <<get>> = 3.141592;
    ' リテラル以外の初期化子は出力しない
    # list : IList<int>
}

未対応

現在対応できていないコードについてまとめます。

ネストクラス

現状、入れ子になった型の定義は、以下の様にそのまま入れ子の状態で出力されるのですが、PlantUMLではエラーとなり変換できません。

'plantuml
'' このコードはエラーになります
class NestedClass {
    + A : int <<get>>
    + B : InnerClass <<get>>
    class InnerClass {
        + X : string <<get>>
        + MethodX() : void
    }
}

内部クラスを外に出して、+-- で結ぶと入れ子を表現可能なので、以下の様に変換すべきなのですが。。。

class NestedClass {
    + A : int <<get>>
    + B : InnerClass <<get>>
}
class InnerClass {
    + X : string <<get>>
    + MethodX() : void
}
NestedClass +-- InnerClass

http://pierre3net.azurewebsites.net/Content/image/NestedClass.svg

演算子オーバーロード

演算子オーバーロードを定義してもPlantUMLに出力されません。(未検討)


変換例

最後に、いろいろな変換例を載せておきます。

変換前

変換元C#のコードはこちらを参照ください InputClasses.cs

変換後のPlantUML
'plantuml
class ClassA {
    - <<readonly>> intField : int = 100
    - {static} strField : string
    # X : double = 0
    # Y : double = 1
    # Z : double = 2
    - list : IList<int>

    # PropA : int <<get>>
    # <<internal>> PropB : string <<get>> <<protected set>>
    <<internal>> PropC : double <<get>> = 3.141592
    + ClassA()
    {static} ClassA()
    # <<virtual>> VirtualMethod() : void
    + <<override>> ToString() : string
    + {static} StaticMethod() : string
}
abstract class ClassB {
    - field_1 : int
    {abstract} + PropA : int <<get>> <<protected set>>
    # <<virtual>> VirtualMethod() : string
    + {abstract} AbstractMethod(arg1:int, arg2:double) : string
}
class ClassC <<sealed>> {
    - {static} <<readonly>> readonlyField : string = "ReadOnly"
    + <<override>> PropA : int <<get>> <<protected set>> = 100
    + <<override>> AbstractMethod(arg1:int, arg2:double) : string
    # <<override>> VirtualMethod() : string
}
class Vector <<struct>> {
    + X : double <<get>>
    + Y : double <<get>>
    + Z : double <<get>>
    + Vector(x:double, y:double, z:double)
    + Vector(source:Vector)
}
enum EnumA {
    AA= 0x0001,
    BB= 0x0002,
    CC= 0x0004,
    DD= 0x0008,
    EE= 0x0010,
}

ClassB <|-- ClassC
クラス図

http://pierre3net.azurewebsites.net/Content/image/inputClasses.svg

Roslyn で C#のソースコードからPlantUMLのクラス図を生成する

前回 に引き続き、PlantUMLの話になります。

ReactiveProperty の記事もまだ途中なのですが、もうちょっと寄り道します。

今回は、Roslynで C#ソースコードから PlantUMLのクラス図を生成するプログラムを作ってみよう!というお話です。

Roslyn を使って C#ソースコードから PlantUML のクラス図を生成してみる

Roslyn でメタプログラミング

Roslynの導入方法、Roslynを使ったプログラミングについては、Build Insiderの記事が参考になります。

www.buildinsider.net

www.buildinsider.net

サンプルコード

今回も、サンプルコードをGitHubに上げておきました。

github.com

CSharpSyntaxTree および CSharpSyntaxWalker

今回のプログラムですが、基本的な仕組みは以下のようになります。

  1. ソースコード構文木(CSharpSyntaxTree)にパース
  2. CSharpSyntaxWalker を継承したクラスを用意して、必要な要素をVisitorパターンで訪問
  3. 訪問先で、取得した情報を加工して出力

といった流れで、いたってシンプルです。

using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

class Program
{
    static void Main(string[] args)
    {
        //ソースコード
        var code = @"class Hoge{ ...(略)"; 
        //SyntaxTree にパース
        var tree = CSharpSyntaxTree.ParseText(code);
        var root = tree.GetRoot();
        
        //出力先。今回はStringWriterで書き込みます
        var output = new StringBuilder();
        using (var writer = new StringWriter(output))
        {
            //CSharpSyntaxWalker を継承したクラス
            //この中で変換処理やら出力やらをやります
            var gen = new ClassDiagramGenerator(writer, indent:"    ");
            gen.Visit(root);
        }
        Console.WriteLine(output);
    }
}

ClassDiagramGenerator

上記のコードで重要なのが、ClassDiagramGenerator で、このプログラムの心臓部になります。
CSharpSyntaxWalker を継承しています。

では、ClassDiagramGenerator をクラス図で見てみましょう。

http://pierre3net.azurewebsites.net/Content/image/ClassDiagramGenerator.svg

Visit~ と名前の付いたメソッドがいくつかあります。
これらはCSharpSyntaxWalker からオーバーライドしたものです。構文木を辿り、対応するノード(ここではクラス、インターフェース、プロパティ、メソッド等の定義部)を訪問した際に実行されます。
このメソッド内に、必要な情報を取得し、適切な文字列に変換して出力する処理を記述します。

細かな実装については、割愛させていただきます。 Roslynのおかげでとてもシンプルなコードになっていると思いますので、詳細は以下のソースコードで確認してみてください。

ClassDiagramGenerator.cs

変換例

実際に、C#のクラス定義がどのように変換されるかの一例を示します。

C# (ClassA.cs)

public class ClassA
{
    private readonly int intField = 100;
    private static string strField;
    protected double X = 0, Y = 1, Z = 2;

    protected int PropA { get; private set; }
    protected internal string PropB { get; protected set; }
    internal double PropC { get; } = 3.141592;

    public ClassA() { }
    static ClassA() { strField = "static field"; }

    protected virtual void VirtualMethod() { }
    public override string ToString()
    {
        return intField.ToString();
    }

    public static string StaticMethod() { return strField; }
}

PlantUML (ClassA.plantuml)

class ClassA {
    - <<readonly>> intField : int = 100
    - {static} strField : string
    # X : double = 0
    # Y : double = 1
    # Z : double = 2
    # PropA : int <<get>>
    # <<internal>> PropB : string <<get>> <<protected set>>
    <<internal>> PropC : double <<get>> = 3.141592
    + ClassA()
    {static} ClassA()
    # <<virtual>> VirtualMethod() : void
    + <<override>> ToString() : string
    + {static} StaticMethod() : string
}

クラス図 (ClassA.svg)
http://pierre3net.azurewebsites.net/Content/image/ClassA.svg

オブジェクト間のリンク

複数オブジェクトの関連付けなどには対応していません。
これは、別途手書きで行います。

@startuml
!include ClassA.plantuml
!include ClassB.plantuml

IDisposable ()-- ClassA
ClassBBase <|-- ClassB
ClassA o- ClassB : aggregation
@enduml

PlantUMLでは、!include [ファイル名] で別のPlanuUMLファイルを読み込んで使用することが出来ます。
このように、クラス単位で自動生成したファイルをIncludeして、関連のみを別ファイルに記載するとスッキリして良いです。

http://pierre3net.azurewebsites.net/Content/image/ClassDiagram.svg

まとめ

今回の様に、C#のコードから他の言語(DSL)などを生成するといった事が、 Roslyn の力で いとも簡単に出来てしまうことが分かりました。

むしろ生成処理そのものよりも、C#の型定義とUMLとの対応付けをどうするかを決める作業の方が難しいのではないでしょうか。

C#に固有の、UMLでは定義されていない(もしくは微妙に意味が異なる)要素をどう扱うかが非常に悩ましいところでした。
今回は、なるべく変換処理が複雑にならないように、というのも加味して、今の実装に落ち着いたのですが...

この辺りの話は、また別の記事でまとめたいと思います

PlantUMLを自動変換してLiveプレビューしてくれるAtom拡張 PlantUML-Viewer がイイ!

PlantUML とは

テキストベースでUMLのダイアグラムをサクサクかける ドメイン固有言語(DSL)です。

以下のような特徴があります。

  • テキストベースなので、バージョン管理ツールでの差分管理が可能
  • 図のレイアウトを自動でやってくれるので、書きたい内容に集中できる
  • PNGでの出力以外に SVG、EPS 等のベクタ形式での出力が可能

参照

Atom Editor でPlantUML

とても便利なPlantUMLなのですが、テキストをUMLに変換して、結果を確認するまでの手順が少々面倒であったりします。

  1. PlantUMLの書式に従ってテキストを編集する
  2. テキストファイルをUMLに変換して、画像ファイルとして出力
  3. できたファイルを画像ビューアやWebブラウザ等で開き、結果を確認する
  4. 1 に戻る

この辺りの手順を自動化して、結果をプレビュー表示してくれるツールを探していたのですが、最近見つけたAtom Editorのプラグインが素敵だったので、紹介したいと思います。

Atom

atom.io

Atom (テキストエディタ) - Wikipedia

plantuml-viewer

atom.io

  • PlantUMLのテキストをUMLに変換して、エディタ右側の分割ペインに表示してくれます。
  • テキストの編集に追従して、リアルタイムに図が更新されます。(Live Preview)
  • 図はマウス操作で自由に拡大/縮小、移動することができます。
  • 図は、PNG, SVG, EPS の3種類の形式で保存可能です。

導入手順

PlantUMLを使う準備

PlantUMLを使用するには、以下がインストールされている必要があります。

Java ランタイム(JRE)

PlanuUMLはJavaで動くので、Javaのランタイムがインストールされている必要があります。
インストールされていない場合は、以下から最新版をインストールすればよいと思います。

http://java.com/ja/

Graphviz

シーケンス図以外の図のレンダリングには、 Graphiviz の dot.exe というモジュールが必要になります。

http://www.graphviz.org/Download..php

なお、plantuml本体のモジュール plantuml.jar はplantuml-viewer に組み込まれているので別途インストールする必要はありません

Atomのインストール

以下のリンクからインストーラをダウンロードして、普通にインストールするのが一番簡単です。

Atom

Atom プラグインのインストール

Atomを起動したら、[File]メニューのSettings を開き、左側で[Install]を選ぶと、以下の画面になります。 ここで、”plantuml” と入力して表示される一覧からplantuml-viewer を探して[Install]ボタンをクリックします。 f:id:pierre3:20150824224437p:plain

ついでに、PlantUMLで書かれたテキストをシンタックスハイライト表示してくれるパッケージ language-plantuml も入れておきましょう。

plantuml-viewer の設定

次に、plantuml-viewer の設定を行います。

先ほどのSettingsページで、今度は[Packages]を選択します。
すると、インストールしたプラグインのパッケージ一覧が表示されますので、ここでplantuml-viewer を探して[Settings]ボタンをクリックします。 f:id:pierre3:20150824224813p:plain f:id:pierre3:20150824224824p:plain

設定項目

  • Charset
    変換前のテキストのエンコーディングを指定します。指定しないとシステムのデフォルト(日本語環境のWindowsではCP932(Shift-jis))になります。

    当初、この設定項目がなく、Shift-jis以外で書くと文字化けしていました。この項目を指定できるようにカスタマイズして、プルリクエストしてみたところ、即日取り込んでいただき、V0.60から使用可能となっています。(感謝)

  • Config File
    変換時に読み込まれる、共通の処理を記載しておくplantUMLファイルのパスを指定します。図の外観のカスタマイズする場合などに使用します。

  • Grammars
    対象となるファイルの拡張子を指定します。
  • Graphviz Dot Executable
    インストールした Graphivizの dot.exe のパスを指定します。
  • Live Update
    チェックを外すと、図の自動更新を無効にできます。
  • Open in Split Pane
    チェックを外すと、図のプレビューを分割ペインではなく、通常のタブに表示するようになります。

最後に

plantuml-viewer はまだリリース間もない(8/9)のと、私も使い始めたばかりなので、もう少し使い込んでみる必要はあるかと思いますが、第一印象としてはかなりいい感じです。

この記事を読んで気になった方は、ぜひ試してみてください!

※ 2016/3/6追記

変換するUMLのサイズが大きいと、極端に重くなったり、入力したテキストの内容がちゃんと反映されなかったりする場合があります。
この辺の話と対処法を以下の記事に書きましたので、こちらもご参照ください。

pierre3.hatenablog.com