ようこそ。睡眠不足なプログラマのチラ裏です。

カスタム ビジュアライザって「刺身たんぽぽ」になりがちだよね。楽にカスタム ビジュアライザを量産して「たのしいデバッグ」を。


デバッガ ビジュアライザとは

デバッガ ビジュアライザとは、Visual Studioデバッグをしている時に、
オブジェクトの状態を照会することが出来る画面および、その機能のことです。
そうです。いつもお世話になっているアレです。たとえば、string型についてデフォルトで提供されている
テキストビジュアライザやXMLビジュアライザ、HTMLビジュアライザなどがそれにあたります。

 



デバッガ ビジュアライザのアーキテクチャには、デバッガ側とデバッグ対象側の2つがあって、
1つはデバッガ側ドメインで、もう1つはVisual Studio デバッグ側のドメインで動作し、互いに協調して動作します。


カスタム ビジュアライザとは

Visual Studio 2005以降のデバッガは非常に強力で、
ただ強力なだけではなく自らがデバッガの機能を拡張することが可能となっています。
その拡張の1つが、カスタム ビジュアライザです。
使いやすいカスタム ビジュアライザを準備しておけば、デバッグの効率が飛躍的に向上します。


カスタム ビジュアライザの基本的な作成方法については、「チュートリアル : C# でビジュアライザを記述する」にて解説があります。


カスタム ビジュアライザの対象にできる型に関する注意

カスタム ビジュアライザでは、Object型 および Array型 を除く任意のマネージ クラスについて作成可能です。
また、ジェネリック型のサポートには制限があり、ジェネリック型がオープン構築型の場合にのみに限られます。
クローズ構築型のジェネリックはサポートされません。


カスタム ビジュアライザの対象にできる型には、更に把握しておくべき条件があります。
ビジュアライザは前述したようにデバッガ側とデバッグ対象側の2つのアーキテクチャによって動作します。
これは、それぞれ異なる世界です。.NETの言葉で言えば、異なるアプリケーションドメインで動作していると言います。
つまり、ビジュアライザの対象とされるオブジェクトは、デバッグ対象側からデバッガ側へと
異なるアプリケーションドメイン間で通信されることを意味しています。
ですから、ビジュアライザの対象にできるオブジェクトの型は MarshalByRefObject から派生しているか、
あるいは Serializable属性がマークされていてシリアル化可能である必要があるというわけです。


では、ビジュアライザの対象としたいオブジェクトが MarshalByRefObject の派生型でもなく、
シリアライズ可能でもなかった場合はビジュアライザが使えないことになります。どうすればよいでしょう。
そのような場合は、オブジェクトを構成するのに最低限必要な情報から構成した、
シリアル化可能な(Serializable属性でマークされた)プロキシクラスを用意します。
VisualizerObjectSourceの派生型を定義し、GetData(object target, Stream outgoingData)をオーバーライドし、
プロキシクラスをシリアル化するよう実装します。つまり異なるドメインについてプロキシを通じてやり取りするようにします。


カスタム ビジュアライザの作成は「刺身たんぽぽ」になりがち

基本的な作成方法について理解し、いくつかのカスタム ビジュアライザを作成すると、
いくつも似たような実装を繰り返していることにすぐに気が付くでしょう。
型について異なるだけで、似たような実装がたくさん現れます。ちょっと待って。これって「刺身たんぽぽ」じゃね?
「いいじゃんいいじゃん。別にコピペすりゃあ済む話。」、「ぼく刺身たんぽぽが天職ですからッ!(キリッ」
いやいやいやいや、それないわーーー!そんなこと言わないでジェネリック活用してください。お願いします。


ということで、ジェネリックを活用した実装サンプルを以下に示します。



ジェネリックを活用したDebuggerVisualizer作成サンプル

 


DialogDebuggerVisualizerのジェネリック表現と、VisualizerObjectSourceのジェネリック表現です。
この実装だけを見ても、いまいちピンとこないと思います。他のコードと合わせて見てください。

using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using Microsoft.VisualStudio.DebuggerVisualizers;

namespace DebuggerVisualizers
{
    public class DialogDebuggerVisualizer<TTarget, TVisualizer> : DialogDebuggerVisualizer
        where TVisualizer : DialogDebuggerVisualizer<TTarget, TVisualizer>
    {
        protected override void Show(IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider)
        {
            var obj = (TTarget)objectProvider.GetObject();
            obj.Show<TTarget, TVisualizer>(windowService, objectProvider);
        }
    }

    public class VisualizerObjectSourceProxy<TTarget, TProxy> : VisualizerObjectSource
    {
        public override void GetData(object target, Stream outgoingData)
        {
            BinaryFormatter bf = new BinaryFormatter();
            bf.Serialize(outgoingData, ((TTarget)target).CreateProxy<TTarget,TProxy>());
        }
    }
}


「ビジュアライザの対象とする型」と、「カスタム ビジュアライザの型」ごとについての動作に関する定義などなど

using System;
using System.Security.Cryptography;
using Microsoft.VisualStudio.DebuggerVisualizers;

namespace DebuggerVisualizers
{
    public static class DebuggerVisualizerExtentions
    {
        private class ShowTypeSelector<TTarget,TVisualizer>
        {
            public static Action<TTarget, IDialogVisualizerService, IVisualizerObjectProvider> debuggerVisualizerShow = (obj, ws, op) => { };

            static ShowTypeSelector()
            {
                ShowTypeSelector<TTarget,TVisualizer>.debuggerVisualizerShow = (obj, ws, op) => { };
                ShowTypeSelector<Uri,UriVisualizer>.debuggerVisualizerShow = (obj, ws, op) => obj.DebuggerVisualizerUriShow(ws, op);
                ShowTypeSelector<Uri,UriPropertyGridVisualizer>.debuggerVisualizerShow = (obj, ws, op) => obj.DebuggerVisualizerPropertyGridShow(ws, op);
                ShowTypeSelector<String,StringUriVisualizer>.debuggerVisualizerShow = (obj, ws, op) => obj.DebuggerVisualizerShow(ws, op);
                ShowTypeSelector<String, StringPropertyGridVisualizer>.debuggerVisualizerShow = (obj, ws, op) => obj.DebuggerVisualizerPropertyGridShow(ws, op);
                ShowTypeSelector<RijndaelManagedProxy, RijndaelManagedPropertyGridVisualizer>.debuggerVisualizerShow = (obj, ws, op) => obj.DebuggerVisualizerPropertyGridShow(ws, op);
            }
        }

        public static void Show<TTarget,TVisualizer>(this TTarget self, IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider)
        {
            ShowTypeSelector<TTarget,TVisualizer>.debuggerVisualizerShow(self, windowService, objectProvider);
        }

        private class CreateTypeSelector<TTarget, TProxy>
        {
            public static Func<TTarget, TProxy> createProxy = target => { return default(TProxy); };

            static CreateTypeSelector()
            {
                CreateTypeSelector<TTarget, TProxy>.createProxy = target => { return default(TProxy); };
                CreateTypeSelector<RijndaelManaged, RijndaelManagedProxy>.createProxy = target => { return new RijndaelManagedProxy(target); };
            }
        }

        public static TProxy CreateProxy<TTarget, TProxy>(this TTarget target)
        {
            return CreateTypeSelector<TTarget, TProxy>.createProxy(target);
        }
    }
}

各DialogDebuggerVisualizerです。

using System;

namespace DebuggerVisualizers
{
    public class UriVisualizer : DialogDebuggerVisualizer<Uri,UriVisualizer> { }
    public class UriPropertyGridVisualizer : DialogDebuggerVisualizer<Uri, UriPropertyGridVisualizer> { }
    public class StringUriVisualizer : DialogDebuggerVisualizer<String, StringUriVisualizer> { }
    public class StringPropertyGridVisualizer : DialogDebuggerVisualizer<String, StringPropertyGridVisualizer> { }
    public class IntVisualizer : DialogDebuggerVisualizer<int, IntVisualizer> { }

    #region Proxy
    public class RijndaelManagedPropertyGridVisualizer : DialogDebuggerVisualizer<RijndaelManagedProxy, RijndaelManagedPropertyGridVisualizer> { }
    #endregion
}

VisualizerObjectSourceです。

using System.Security.Cryptography;

namespace DebuggerVisualizers
{
    public class RijndaelManagedVisualizerObjectSource : VisualizerObjectSourceProxy<RijndaelManaged, RijndaelManagedProxy> { }
}


カスタム ビジュアライザ対象の型であるUriについての、ビジュアライザ表示方法の具体的な実装です。
UriをWebBrowserで表示するDebuggerVisualizerUriShowと、PropertyGridで表示するDebuggerVisualizerPropertyGridShowを
拡張メソッドで実装しています。objectProvider.ReplaceObjectによって、
ビジュアライザ対象オブジェクトの値を書き換えることができます。

using System;
using Microsoft.VisualStudio.DebuggerVisualizers;

namespace DebuggerVisualizers
{
    public static class UriExtentions
    {
        public static void DebuggerVisualizerUriShow(this Uri self, IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider)
        {
            using (var form = new UriVisualizerForm())
            {
                form.webBrowser1.Navigate(self);
                windowService.ShowDialog(form);

                if (form.DialogResult != System.Windows.Forms.DialogResult.OK)
                    return;
                if (!objectProvider.IsObjectReplaceable)
                    return;

                objectProvider.ReplaceObject(new Uri(form.textBox1.Text));
            }
        }

        public static void DebuggerVisualizerPropertyGridShow(this Uri self, IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider)
        {
            using (var form = new PropertyGridVisualizer())
            {
                form.propertyGrid1.SelectedObject = self;
                windowService.ShowDialog(form);
            }
        }
    }
}


カスタム ビジュアライザ対象の型であるstringについての、ビジュアライザ表示方法の具体的な実装です。

using System;
using Microsoft.VisualStudio.DebuggerVisualizers;

namespace DebuggerVisualizers
{
    public static class StringExtentions
    {
        public static void DebuggerVisualizerShow(this string self, IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider)
        {
            using (var form = new UriVisualizerForm())
            {
                form.webBrowser1.Navigate(new Uri(self));
                windowService.ShowDialog(form);

                if (form.DialogResult != System.Windows.Forms.DialogResult.OK)
                    return;
                if (!objectProvider.IsObjectReplaceable)
                    return;

                objectProvider.ReplaceObject(form.textBox1.Text);
            }
        }

        public static void DebuggerVisualizerPropertyGridShow(this string self, IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider)
        {
            using (var form = new PropertyGridVisualizer())
            {
                form.propertyGrid1.SelectedObject = self;
                windowService.ShowDialog(form);
            }
        }
    }
}


RijndaelManagedはシリアル化不可であるため、シリアル化可能なRijndaelManagedProxyというプロキシクラスを作ります。

using System;
using System.Security.Cryptography;

namespace DebuggerVisualizers
{
    [Serializable]
    public class RijndaelManagedProxy
    {
        public byte[] IV { get; private set; }
        public byte[] Key { get; private set; }

        public RijndaelManagedProxy(RijndaelManaged target)
        {
            IV = target.IV;
            Key = target.Key;
        }
    }
}

カスタム ビジュアライザ対象の型であるRijndaelManagedProxyについての、ビジュアライザ表示方法の具体的な実装です。

using System;
using System.Windows.Forms;
using Microsoft.VisualStudio.DebuggerVisualizers;

namespace DebuggerVisualizers
{
    public static class RijndaelManagedExtentions
    {
        public static void DebuggerVisualizerPropertyGridShow(this RijndaelManagedProxy self, IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider)
        {
            using (var form = new PropertyGridVisualizer())
            {
                form.propertyGrid1.SelectedObject = self;
                windowService.ShowDialog(form);
            }
        }
    }
}


WebBrowser表示するビジュアライザフォームです。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace DebuggerVisualizers
{
    public partial class UriVisualizerForm : Form
    {
        public UriVisualizerForm()
        {
            InitializeComponent();
            this.Text = "Uri Visualizer";
            this.StartPosition = FormStartPosition.CenterScreen;
        }

        private void webBrowser1_Navigated(object sender, WebBrowserNavigatedEventArgs e)
        {
            if (webBrowser1.Url == null) return;
            textBox1.Text = webBrowser1.Url.ToString();
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            this.DialogResult = System.Windows.Forms.DialogResult.OK;
        }
    }
}


PropertyGrid表示するビジュアライザフォームです。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace DebuggerVisualizers
{
    public partial class PropertyGridVisualizer : Form
    {
        public PropertyGridVisualizer()
        {
            InitializeComponent();
            this.Text = "PropertyGrid ビジュアライザー";
            this.StartPosition = FormStartPosition.CenterScreen;
        }
    }
}

assemblyファイルに以下を記述し、DebuggerVisualizerを宣言します。
「ビジュアライザー」としているのはワザトです。

/// DebuggerVisualizer
[assembly: System.Diagnostics.DebuggerVisualizer(
    typeof(DebuggerVisualizers.UriVisualizer),
    Target = typeof(System.Uri),
    Description = "Uri ビジュアライザー"
)]

[assembly: System.Diagnostics.DebuggerVisualizer(
    typeof(DebuggerVisualizers.UriPropertyGridVisualizer),
    Target = typeof(System.Uri),
    Description = "PropertyGrid ビジュアライザー"
)]

[assembly: System.Diagnostics.DebuggerVisualizer(
    typeof(DebuggerVisualizers.StringUriVisualizer),
    Target = typeof(System.String),
    Description = "Uri ビジュアライザー"
)]

[assembly: System.Diagnostics.DebuggerVisualizer(
    typeof(DebuggerVisualizers.StringPropertyGridVisualizer),
    Target = typeof(System.String),
    Description = "PropertyGrid ビジュアライザー"
)]

[assembly: System.Diagnostics.DebuggerVisualizer(
    typeof(DebuggerVisualizers.RijndaelManagedPropertyGridVisualizer),
    typeof(DebuggerVisualizers.RijndaelManagedVisualizerObjectSource),
    Target = typeof(System.Security.Cryptography.RijndaelManaged),
    Description = "RijndaelManaged ビジュアライザー"
)]

[assembly: System.Diagnostics.DebuggerVisualizer(
    typeof(DebuggerVisualizers.IntVisualizer),
    Target = typeof(int),
    Description = "Int Visualizer(未実装)"
)]

ビジュアライザのテスト
VS2010の単体テストで。


ビジュアライザのテストには、VisualizerDevelopmentHostクラスを用いて行ないます。

using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.VisualStudio.DebuggerVisualizers;
using DebuggerVisualizers;

namespace TestProject1
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void TestShowUriVisualizer()
        {
            var str = "http://www.bing.com/";
            var uri = new Uri(str);
            ShowVisualizer(uri, typeof(UriVisualizer));
            ShowVisualizer(uri, typeof(UriPropertyGridVisualizer));
        }

        [TestMethod]
        public void TestShowStringUriVisualizer()
        {
            var str = "http://www.bing.com/";
            ShowVisualizer(str, typeof(StringUriVisualizer));
            ShowVisualizer(str, typeof(StringPropertyGridVisualizer));
        }

        [TestMethod]
        public void TestShowRijendaeManagedVisualizer()
        {
            var rijndael = new RijndaelManaged();
            ShowRijendaeManagedVisualizer(rijndael
                , typeof(RijndaelManagedPropertyGridVisualizer)
                , typeof(RijndaelManagedVisualizerObjectSource));
        }

        [TestMethod]
        public void TestShowIntVisualizer()
        {
            var i = 100;
            ShowVisualizer(i, typeof(IntVisualizer));
        }

        public static void ShowVisualizer(object objectToVisualize,Type visualizer)
        {
            var visualizerHost = new VisualizerDevelopmentHost(objectToVisualize, visualizer);
            visualizerHost.ShowVisualizer();
        }

        public static void ShowRijendaeManagedVisualizer(object objectToVisualize,Type visualizer,Type visualizerObjectSource)
        {
            var visualizerHost = new VisualizerDevelopmentHost(objectToVisualize, visualizer, visualizerObjectSource);
            visualizerHost.ShowVisualizer();
        }
    }
}


というような感じで実装しておけば、きっと「刺身たんぽぽ」の呪いから開放されることでしょう。
カスタム ビジュアライザ大好きです。どんどん作ってどんどん有効活用して良きデバッグライフを。
VB.NET版とF#版も書こうかと思いましたが、力尽きました。何かの参考になれば幸いです。