Revitアドインのデバッグログをキャプチャする方法

# はじめに
Revitアドインの開発中に`Console.WriteLine`や`Debug.Print`でデバッグログを表示したいと思ったことはありませんか？
この記事では、Revit本体やアドインが出力するデバッグログをキャプチャする方法を紹介します。コードをコピペしたい方は [デバッグログのキャプチャと表示](#デバッグログのキャプチャと表示) を確認してください。

![スクリーンショット 2024-09-08 232942.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059946/521e73e1-4a1b-7cf9-dacf-dd61b2488588.png)

なお、コンソールログを表示・検索するツール "ConsoleCapture" がAutodesk App Storeで無料ダウンロードできます。

https://apps.autodesk.com/RVT/ja/Detail/Index?id=3859068956354628782&appLang=ja&os=Win64


# デバッグログの出力
デバッグログの出力には、主にConsoleクラスを使う方法と、Debug/Traceクラスを使う方法があります。


### Consoleクラス
Consoleログはプログラムとして標準出力や標準エラー出力にメッセージを表示する必要がある場合に使います。

```C#:
// メッセージを標準出力に表示し改行
Console.WriteLine("ConsoleWriteLine"); // 出力: ConsoleWriteLine

// メッセージを標準出力に表示（改行なし）
Console.Write("Console"); Console.Write("Write"); // 出力: ConsoleWrite

// 標準エラー出力に表示し改行
Console.Error.WriteLine("Error message"); // 出力: Error message
```


### Traceクラス
Traceログは製品版でも出力する可能性があるトレースログ出力に使います。標準出力には表示されずトレースリスナー（ログファイルやデバッグウィンドウなど）にメッセージを送ります。

```C#:
// メッセージを出力ウィンドウに表示し改行
Trace.WriteLine("TraceWriteLine"); // 出力: TraceWriteLine

// メッセージを表示（改行なし）
Trace.Write("Trace"); Trace.Write("Write"); // 出力: TraceWrite

// メッセージをフォーマットして表示し改行
Trace.TraceInformation("Trace.{0}", "Information"); // 出力: Trace.Information
```


### Debugクラス
Debugログはデバッグ時のみ必要なレベルのログ出力に使います。標準出力には表示されずデバッグリスナー（Visual Studioの出力ウィンドウなど）にメッセージを送ります。デバッグビルドでのみ有効になりリリースビルドでは無効になります。

```C#:
// メッセージを表示し改行
Debug.WriteLine("DebugWriteLine"); // 出力: DebugWriteLine

// メッセージを表示（改行なし）
Debug.Write("Debug"); Debug.Write("Write"); // 出力: DebugWrite

// メッセージをフォーマットして表示し改行
Debug.Print("Debug.{0}", "Print"); // 出力: Debug.Print
```


# デバッグログのキャプチャと表示
RevitはGUIアプリケーションであるため、コンソールアプリケーションのようにデバッグログを表示することはできません。そのため、標準出力やDebug/Traceリスナーをキャプチャして表示するツールを実装する必要があります。以下にWPF / MVVMパターンで実装した最小限のサンプルコードを示します。


### Model
まず、`TextWriter`を継承してコンソールやデバッグ出力をキャプチャするクラスを用意します。このクラスでは、メッセージを受け取るとバッファに保存し、改行を検出したタイミングでまとめてキャプチャします。キャプチャしたメッセージは`LogCaptured`イベントを介してViewModelに通知されます。

```C#:ConsoleWriter.cs
using System;
using System.IO;
using System.Text;

namespace Addin.Model
{
    public class ConsoleWriter : TextWriter
    {
        private readonly StringBuilder _buffer = new StringBuilder();
        public event Action<string> LogCaptured;

        public ConsoleWriter() {}

        public override Encoding Encoding => Encoding.UTF8;

        // Writeメソッドのオーバーライド（改行なし）
        public override void Write(string value)
        {
            _buffer.Append(value);
            if (value.Contains(Environment.NewLine))
            {
                FlushBuffer();
            }
        }

        // WriteLineメソッドのオーバーライド（1行単位）
        public override void WriteLine(string value)
        {
            _buffer.AppendLine(value);
            FlushBuffer();
        }

        // バッファの内容をフラッシュしてイベントを発生させる
        private void FlushBuffer()
        {
            var completeMessage = _buffer.ToString().TrimEnd();
            _buffer.Clear();
            LogCaptured?.Invoke(completeMessage);
        }
    }
}

```


### ViewModel
次に、メッセージの入れ物となるクラスを用意します。

```C#:LogEntry.cs
using System;

namespace Addin.Model
{
    public class LogEntry
    {
        public string Message { get; set; }
        public int Index { get; set; }
    }
}
```

続いて、Modelからのイベントを受け取り、`LogEntry`のコレクションとしてログデータを保持するクラスを用意します。DebugとTraceはイベントリスナーを共有しているため、`Debug.Listeners`のみにリスナーを追加すれば両方の出力をキャプチャできます。

```C#:ConsolePageVM.cs
using Addin.Model;
using System.Collections.ObjectModel;
using System.Diagnostics;

namespace Addin.ViewModel
{
    internal class ConsolePageVM : ViewModelBase
    {
        public ObservableCollection<LogEntry> Logs { get; set; }

        public ConsolePageVM()
        {
            Logs = new ObservableCollection<LogEntry>();
            SetupConsoleWriters();
        }

        // ConsoleWriterの初期設定
        private void SetupConsoleWriters()
        {
            var consoleWriter = new ConsoleWriter();
            consoleWriter.LogCaptured += OnLogCaptured;
            Console.SetOut(consoleWriter);
            Console.SetError(consoleWriter);
            var listener = new TextWriterTraceListener(consoleWriter);
            Debug.Listeners.Add(listener);
        }

        // ログキャプチャイベントハンドラー
        private void OnLogCaptured(string message)
        {
            App.Current.Dispatcher.Invoke(() =>
            {
                Logs.Add(new LogEntry
                {
                    Index = Logs.Count + 1,
                    Message = message
                });
            });
        }
    }
}
```


### View
最後に、DataGridを使ってViewModelの`Logs`を表示する画面をつくります。

```C#:ConsolePage.xaml
<Page x:Class="Addin.Views.ConsolePage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:vm="clr-namespace:Addin.ViewModel"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      Title="Console Capture">
      
    <Page.DataContext>
        <vm:ConsolePageVM/>
    </Page.DataContext>
    
    <Grid>
        <DataGrid ItemsSource="{Binding Logs}" AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Roq" Binding="{Binding Index}" Width="Auto"/>
                <DataGridTextColumn Header="Message" Binding="{Binding Message}" Width="*"/>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Page>

```


### 実行
試しに以下のようなコマンドを作成してデバッグログを出力してみます。

```C#:Dummy.cs
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace Addin.Commands
{
    [Autodesk.Revit.Attributes.Transaction(Autodesk.Revit.Attributes.TransactionMode.Manual)]
    internal class Dummy : IExternalCommand
    {
        public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
        {
            try
            {
                Console.WriteLine("Console.WriteLine");
                Console.Write("Console"); Console.Write("Write"); Console.Write(Environment.NewLine);
                Console.Error.WriteLine("Console.Error.WriteLine");
                
                Debug.WriteLine("Debug.WriteLine");
                Debug.Write("Debug."); Debug.Write("Write"); Debug.Write(Environment.NewLine);
                Debug.Print("Debug.{0}", "Print");
                
                Trace.WriteLine("Trace.WriteLine");
                Trace.Write("Trace."); Trace.Write("Write"); Trace.Write(Environment.NewLine);
                
                var dict = new Dictionary<string, Dictionary<string, string>>()
                {
                    {
                        "key1",
                        new Dictionary<string, string>
                        {
                            { "key1-1", "value1-1" },
                            { "key1-2", "value1-2" }
                        }
                    },
                    {
                        "key2",
                        new Dictionary<string, string>
                        {
                            { "key2-1", "value2-1" },
                            { "key2-2", "value2-2" }
                        }
                    },
                };
                var json = System.Text.Json.JsonSerializer.Serialize(dict, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
                Console.WriteLine(json);

                return Result.Succeeded;
            }

            catch (Exception ex)
            {
                // コマンドが途中で失敗した際のエラー
                TaskDialog.Show("ERROR", ex.Message);
                return Result.Failed;
            }
        }
    }
}

```

以下のようにデバッグログが表示されます。`System.Text.Json`でJSONをシリアル化する際に`WriteIndented = true`オプションを設定することでJSONも整形された状態で表示できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059946/15f2e4d8-ac2e-9ae7-0813-a162760a46cc.png)


# 参考リンク

https://learn.microsoft.com/ja-jp/troubleshoot/developer/visualstudio/csharp/language-compilers/trace-and-debug

https://learn.microsoft.com/ja-jp/dotnet/api/system.diagnostics.debug?view=net-8.0

https://learn.microsoft.com/ja-jp/dotnet/api/system.diagnostics.textwritertracelistener?view=net-8.0