FromEventが面倒なので自動生成させてみた2

C# Reactive Extensionsネタ

前の日記で書いた自動生成ロジックが
継承を意識していなかったため基底クラスの同じメソッドを生成していたので、
クラス毎にメソッドを生成するようにしてみた。

<#@ template language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Windows.Forms" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Reflection" #>
<#@ import namespace="System.Windows.Forms" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>

using System;
using System.Collections.Generic;
using System.Linq;

internal static class ControlEventExtensions
{
<#  // ここにGenerateCode(params Type[] types)に任意の型を入れて書く
    // 例 GenerateCode(typeof(PictureBox),typeof(Panel))
#>
<#= GenerateCode(typeof(PictureBox),typeof(Panel)) #>
}


<#+
    /// <summary>
    /// メソッドテンプレート
    /// </summary>
    private const string MethodCodeTemplate = "\r\n" +
    "    public static IObservable<IEvent<{0}>> Get{1}(this {2} that)\r\n" +
    "    {{\r\n" +
    "        return Observable.FromEvent<{0}>(that, \"{1}\");\r\n" +
    "    }}\r\n";

    /// <summary>
    /// 与えられた型リストから継承関係を意識してコードを生成する。
    /// </summary>
    /// <param name="types">自動生成させる型</param>
    /// <returns></returns>
    public static string GenerateCode(params Type[] types)
    {
        // 指定されたすべての型に対して、現在の型と継承元の型をリストにする。
        var typeList = types
            // 型を展開する
            .SelectMany(t => GetParentType(t))
            // 同一の型は1つのみにする
            .Distinct()
            // ソートして順序を固定化しておく
            .OrderBy(t => t.FullName)
            // リストの取得
            .ToList();

        var sb = new StringBuilder(1000);
        foreach (var type in typeList) {
            sb.Append(GenerateCode(type));
        }
        return sb.ToString();
    }

    /// <summary>
    /// 型の継承ツリーをたどる
    /// </summary>
    /// <param name="type"></param>
    /// <returns></returns>
    public static IEnumerable<Type> GetParentType(Type type)
    {
        var prevType = type;
        while (prevType != typeof(object)) {
            yield return prevType;
            prevType = prevType.BaseType;
        }
    }

    /// <summary>
    /// コード生成
    /// </summary>
    /// <param name="type"></param>
    /// <returns></returns>
    public static string GenerateCode(Type type)
    {
        // ベースの型を取得
        var events = type.GetEvents(
            BindingFlags.Public |
            BindingFlags.InvokeMethod |
            BindingFlags.DeclaredOnly |
            BindingFlags.Instance
            );
        // イベントのリストからイベント名とイベントの型のリストを取得する
        var list = events.Select(ev => new { Handler = ev.EventHandlerType, Name = ev.Name })
                            .Select(ev => new {
                                HandlerType = ev.Handler,
                                Parameters = GetDelegateParameterTypes(ev.Handler),
                                ev.Name
                            })
                            .ToList();
        var list2 = list
            // 引数2以外のやつは存在するのかよくわからないのでとりあえず読み込まない
            .Where(v => v.Parameters.Length == 2)
            .Select(v => string.Format(MethodCodeTemplate,
                                        v.Parameters[1].FullName,
                                        v.Name,
                                        type.FullName));


        return string.Join("", list2.ToArray());
    }

    /// <summary>
    /// デリゲートの型からパラメータの型を取得する
    /// </summary>
    /// <see>http://msdn.microsoft.com/ja-jp/library/ms228976(VS.95).aspx</see>
    /// <param name="d"></param>
    /// <returns></returns>
    private static Type[] GetDelegateParameterTypes(Type d)
    {
        if (d.BaseType != typeof(MulticastDelegate)) {
            throw new InvalidOperationException("Not a delegate.");
        }

        MethodInfo invoke = d.GetMethod("Invoke");
        if (invoke == null) {
            throw new InvalidOperationException("Not a delegate.");
        }

        ParameterInfo[] parameters = invoke.GetParameters();
        Type[] typeParameters = parameters.Select(p => p.ParameterType).ToArray();

        return typeParameters;
    }
    
#>

で、生成されたコードがこれ

using System;
using System.Collections.Generic;
using System.Linq;

internal static class ControlEventExtensions
{

    public static IObservable<IEvent<System.EventArgs>> GetDisposed(this System.ComponentModel.Component that)
    {
        return Observable.FromEvent<System.EventArgs>(that, "Disposed");
    }

    public static IObservable<IEvent<System.EventArgs>> GetAutoSizeChanged(this System.Windows.Forms.Control that)
    {
        return Observable.FromEvent<System.EventArgs>(that, "AutoSizeChanged");
    }

    public static IObservable<IEvent<System.EventArgs>> GetBackColorChanged(this System.Windows.Forms.Control that)
    {
        return Observable.FromEvent<System.EventArgs>(that, "BackColorChanged");
    }

    // とても長いので省略

    public static IObservable<IEvent<System.Windows.Forms.KeyPressEventArgs>> GetKeyPress(this System.Windows.Forms.PictureBox that)
    {
        return Observable.FromEvent<System.Windows.Forms.KeyPressEventArgs>(that, "KeyPress");
    }

    public static IObservable<IEvent<System.EventArgs>> GetLeave(this System.Windows.Forms.PictureBox that)
    {
        return Observable.FromEvent<System.EventArgs>(that, "Leave");
    }

    public static IObservable<IEvent<System.Windows.Forms.ScrollEventArgs>> GetScroll(this System.Windows.Forms.ScrollableControl that)
    {
        return Observable.FromEvent<System.Windows.Forms.ScrollEventArgs>(that, "Scroll");
    }

}

これでComponent.DisposedやControlのメソッド群が複数回生成されなくなったので、
精神衛生上よろしくなった気がします。

前回挙げた課題の

  • 継承を意識していないため、クラスを複数個登録すると同じようなメソッドが大量に生成される(Controlで定義している奴とか)。

    解決

  • GenerateCode()/GenerateCode(typeof(Panel))/GenerateCode("Panel")のどれががいいのかよくわからない

    typeofに決定

  • メソッドテンプレートあたりがスマートじゃない気がする

    未解決

  • 適切なドキュメントコメントがほしい(無理っぽい?)

    ドキュメントコメントはメタデータとして存在しないので無理

FromEventが面倒なので自動生成させてみた。

前の日記で自動生成できないかなぁと言っていたものが
T4 Template使えばできそうだったのでやってみた。
インテリセンスの効かないコーディングはとてもストレスが溜まるものでした。

<#@ template language="C#v3.5" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Windows.Forms" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Reflection" #>
<#@ import namespace="System.Windows.Forms" #>

using System;
using System.Collections.Generic;
using System.Linq;

internal static class ControlEventExtensions
{
	<# // ここにGenerateCode<T>()でTに任意の型を入れて書く #>
	<#= GenerateCode<PictureBox>() #>	
	<#= GenerateCode<Panel>() #>
}


<#+
    /// <summary>
    /// メソッドテンプレート
    /// </summary>
    private static readonly string MethodCodeTemplate = "\r\n" +
    "{3}public static IObservable<IEvent<{0}>> Get{1}(this {2} that)\r\n" +
    "{3}{{\r\n" +
    "{3}    return Observable.FromEvent<{0}>(that, \"{1}\");\r\n" +
    "{3}}}";

    /// <summary>
    /// コード生成
    /// </summary>
    /// <typeparam name="T">生成対象のクラス</typeparam>
    /// <returns></returns>
    public static string GenerateCode<T>()
    {
		var padding = "        ";
        // ベースの型を取得
        var type = typeof(T);
        var events = type.GetEvents();
        // イベントのリストからイベント名とイベントの型のリストを取得する
        var list = events.Select(ev => new { Handler = ev.EventHandlerType, Name = ev.Name })
                         .Select(ev => new {
                             HandlerType = ev.Handler,
                             Parameters = GetDelegateParameterTypes(ev.Handler),
                             ev.Name
                         })
                         .ToList();
        var list2 = list
            // 引数2以外のやつは存在するのかよくわからないのでとりあえず読み込まない
            .Where(v => v.Parameters.Length == 2)
            .Where(v => v.Parameters[1].IsSubclassOf(typeof(EventArgs)) || v.Parameters[1]==typeof(EventArgs))
            .Select(v => string.Format(MethodCodeTemplate,
                                        v.Parameters[1].FullName,
                                        v.Name,
                                        type.FullName,
                                        padding));

        return string.Join("\r\n", list2.ToArray());
    }

    /// <summary>
    /// デリゲートの型からパラメータの型を取得する
    /// </summary>
    /// <see>http://msdn.microsoft.com/ja-jp/library/ms228976(VS.95).aspx</see>
    /// <param name="d"></param>
    /// <returns></returns>
    private static Type[] GetDelegateParameterTypes(Type d)
    {
        if (d.BaseType != typeof(MulticastDelegate)) {
            throw new InvalidOperationException("Not a delegate.");
        }

        MethodInfo invoke = d.GetMethod("Invoke");
        if (invoke == null) {
            throw new InvalidOperationException("Not a delegate.");
        }

        ParameterInfo[] parameters = invoke.GetParameters();
        Type[] typeParameters = parameters.Select(p => p.ParameterType).ToArray();

        return typeParameters;
    }
#>

で、生成されたコードがこれ

using System;
using System.Collections.Generic;
using System.Linq;

internal static class ControlEventExtensions
{
		
        public static IObservable<IEvent<System.EventArgs>> GetCausesValidationChanged(this System.Windows.Forms.PictureBox that)
        {
            return Observable.FromEvent<System.EventArgs>(that, "CausesValidationChanged");
        }

        public static IObservable<IEvent<System.EventArgs>> GetForeColorChanged(this System.Windows.Forms.PictureBox that)
        {
            return Observable.FromEvent<System.EventArgs>(that, "ForeColorChanged");
        }

        public static IObservable<IEvent<System.EventArgs>> GetFontChanged(this System.Windows.Forms.PictureBox that)
        {
            return Observable.FromEvent<System.EventArgs>(that, "FontChanged");
        }

        public static IObservable<IEvent<System.EventArgs>> GetImeModeChanged(this System.Windows.Forms.PictureBox that)
        {
            return Observable.FromEvent<System.EventArgs>(that, "ImeModeChanged");
        }

        public static IObservable<IEvent<System.ComponentModel.AsyncCompletedEventArgs>> GetLoadCompleted(this System.Windows.Forms.PictureBox that)
        {
            return Observable.FromEvent<System.ComponentModel.AsyncCompletedEventArgs>(that, "LoadCompleted");
        }
        // とても長いので途中省略
}

やっていることは単純で

  1. GenerateCodeに型引数を渡す
  2. リフレクションでeventのデリゲートを取得
  3. デリゲートからパラメータを取得
  4. 2引数かつ、第2引数がEventArgsまたはEventArgsから派生したメソッドのリストを取得
  5. メソッドのテンプレートに必要な文字を埋め込んで出力

今後の課題

  • 継承を意識していないため、クラスを複数個登録すると同じようなメソッドが大量に生成される(Controlで定義している奴とか)。
  • GenerateCode()/GenerateCode(typeof(Panel))/GenerateCode("Panel")のどれががいいのかよくわからない
  • メソッドテンプレートあたりがスマートじゃない気がする
  • 適切なドキュメントコメントがほしい(無理っぽい?)

Linq to Eventはこれでかなり快適にインテリセンスが効くようになってイイ感じです。
しかし、VS2008は発売日に買ったのにT4 Templateを知ったのが今月というのは…
なんともったいないことか。

C#でようやくLinq to Eventsが少し理解できた気がする。

Push型のLinq to Objectsとかと違って、
Pull型のLinq to Eventsは動きが想像し辛かったので忘れないようにメモとして残しておく。

//
// 大きい画像をマウスのドラッグでスクロールさせるサンプル
//
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using System.Drawing;

namespace RxTest
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Form form = new Form();
            Panel panel = new Panel { Parent = form, AutoScroll = true, Dock = DockStyle.Fill };
            PictureBox pict = new PictureBox {
                Parent = panel,
                Location = new Point(0, 0),
                Image = new Bitmap(@"D:\test.bmp"),
            };
            pict.Size = pict.Image.Size;
            MouseEventRegister(panel, pict);
            form.ShowDialog();
        }

        private static  void MouseEventRegister(Panel panel,PictureBox pict)
        {
            // マウスによる画像スクロール(Linq to Events)
            // マウスダウンにより発火
            var action = pict.GetMouseDown()
                // ただし左ボタンが押下されていること
                .Where(e => e.EventArgs.Button == MouseButtons.Left)
                // マウスキャプチャ開始(ウィンドウからはみ出てもメッセージを受け取れる)
                .Do(e => pict.Capture = true)
                // イベントの合流(MouseDown + MouseMove)
                .SelectMany(pict.GetMouseMove()
                    // 左ボタンが押されていること
                    .Where(e => e.EventArgs.Button == MouseButtons.Left)
                    // データはEventArgsしかいらない
                    .Select(e => e.EventArgs)
                    // マウスがどれだけ動いたかを保持するために2個ずつの組に変換
                    .BufferWithCount(2)
                    // MouseUpが来るまでがんばる
                    .TakeUntil(pict.GetMouseUp()
                        // 左ボタンのイベント
                        .Where(e => e.EventArgs.Button == MouseButtons.Left)
                        // キャプチャの解除
                        .Do(e => pict.Capture = false)
                ))
                // 移動した大きさを取得
                .Select(v => new Point(v[1].X - v[0].X, v[1].Y - v[0].Y))
                // 購読
                .Subscribe(p => {
                    var pos = panel.AutoScrollPosition;
                    pos.X = 0 - pos.X - p.X;
                    pos.Y = 0 - pos.Y - p.Y;
                    panel.AutoScrollPosition = pos;
                });

        }
    }
    // FromEvent隠蔽用
    internal static class ControlExtensions
    {
        public static IObservable<IEvent<MouseEventArgs>> GetMouseDown(this Control that)
        {
            return Observable.FromEvent<MouseEventArgs>(that, "MouseDown");
        }

        public static IObservable<IEvent<MouseEventArgs>> GetMouseUp(this Control that)
        {
            return Observable.FromEvent<MouseEventArgs>(that, "MouseUp");
        }

        public static IObservable<IEvent<MouseEventArgs>> GetMouseMove(this Control that)
        {
            return Observable.FromEvent<MouseEventArgs>(that, "MouseMove");
        }

    }
}

自分的なポイントは

  • SelectManyでイベントの合流。
  • TakeUntilでイベントが発生するまで続ける。
  • FromEventは拡張メソッドとして定義しておくと楽。(自動生成とかできないかなぁ)

拡張メソッドの部分は以下の記事を参考にさせて頂きました。
http://neue.cc/2009/09/04_197.html