2015年4月15日水曜日

拡張メソッドでイベントを定型化する

.NETでは自作イベントを記述する処理は大体決まっていて、コピペの量産祭になることが多い。
そもそもイベントはメモリリークの原因になることが多いのでRxを使え、との議論もあるがここでは置いておいて。

大体こんな感じのコードが随所に溢れることになる。

//----イベントを宣言
public event HogeEventHandler Hoge;
protected virtual void OnHoge(HogeEventArgs e) {
    if(Hoge != null) {
        Hoge(this,  e);
    }
}

//-----イベントを発生
var e = new HogeEventArgs();
OnHoge(e);

何が嫌かというと、イベントの数だけメソッドが増えてしまうことだ。似たようなコードがずらずらと並んでいるのはとても見栄えが悪い。

そこで半分くらいはネタなのだが、以下のような方法を提案してみる。

public static class EventExtension {
    public static void Fire(this EventArgs e, object sender, dynamic handler) {
        if(handler != null) {
            handler(sender, e);
        }
    }
}

dynamic型を利用しているのが格好悪いが、ハンドラを上手い感じに引数に渡す方法がこれしか見付からなかった。もっと上手い指定の方法があれば良いのだが。

これを定義しておくと、以下のように書くことが出来るようになる。

//----イベントを宣言
public event HogeEventHandler Hoge
//----イベントを発生
var e = new HogeEventArgs();
e.Fire(this, Hoge);

同様にしてCancelEventArgsを拡張してみる。

public static class EventExtension {
    public static void Fire(this CancelEventArgs e, object sender, dynamic before, dynamic after, Action action) {
        if(before != null) {
            before(sender, e);
        }
        if(!e.Cancel) {
            action();
            if(after != null) {
                after(sender, e);
            }
        }
    }
}

これを定義しておくと、 以下のように書くことが出来るようになる。

//----イベントを宣言
public event HogeCancelEventHandler BeforeHoge, AfterHoge;
//----イベントを発生
var e = new HogeCancelEventArgs();
e.Fire(this, BeforeHoge, AfterHoge, ()=> Console.WriteLine("Hogeがキャンセルされなかったので実行したよ!"));

如何だろうか。これで個人的にはかなり楽になったのだが、他にやっている人を見掛けないのでバッドノウハウに分類される技なのかも知れない。

PropertyGridのプロパティ名をリソースを使って日本語化する

.NETで設定画面のGUIを作るとき、PropertyGridを使う人は多いだろう。
その際、例えばこんなモデルを設定したとする。

public class AppProperty {
    [Category("基本情報"), Description("名前です。")]
    public string Name { get; set; }
}

すると、画面には「Name」と表示される。
「Name」ならまだ良いが、これがもっと複雑なプロパティ名だった場合は、使いづらくて仕方がない。

解決策としては @IT さんの PropertyGridコントロールに表示されるプロパティ名を変更するには? に自作のAttributeを使う方法が提示されている。非常に有用な方法なのでそのままコピーして再利用されている方も多いだろう。

その方法では、こんな感じにモデルを作ることになる。

[TypeConverter(typeof(PropertyDisplayConverter))]
public class AppProperty {
    [Category("基本情報"), PropertyDisplayName("名前"), Description("名前です。")]
    public string Name { get; set; }
}

大抵の場合は、上記の方法で必要十分だろう。

だが、例えば以下のような場合では上記以外の方法が必要になる。
  • 多言語対応したアプリを作りたい
  •  同じ名前のプロパティが一杯あって、同じ属性の記述を量産するのは何か違う気がする。
こういう用途には、リソースファイルが非常に相性が良い。
つまり、属性からではなくリソースファイルからプロパティ名を取得するようなTypeConverterを自作してやれば良い。

と言う訳で LocalizableConverter なるものを作ってみた。

public class LocalizableConverter<RESOURCES>: TypeConverter {
    public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes) {
        return new PropertyDescriptorCollection(
            TypeDescriptor.GetProperties(value, attributes, true)
            .Cast<PropertyDescriptor>()
            .Select(original => new LocalazablePropertyDescriptor(original)).ToArray());
    }
    public override bool GetPropertiesSupported(ITypeDescriptorContext context) {
        return true;
    }
    public class LocalazablePropertyDescriptor: PropertyDescriptor {
        readonly PropertyDescriptor original;
        public LocalazablePropertyDescriptor(PropertyDescriptor original)
            : base(original) {
            this.original = original;
        }
        public override bool CanResetValue(object component) {
            return original.CanResetValue(component);
        }
        public override Type ComponentType {
            get { return original.ComponentType; }
        }
        public override object GetValue(object component) {
            return original.GetValue(component);
        }
        public override string Description {
            get { return original.Description; }
        }
        public override string Category {
            get { return original.Category; }
        }
        public override bool IsReadOnly {
            get { return original.IsReadOnly; }
        }
        public override void ResetValue(object component) {
            original.ResetValue(component);
        }
        public override bool ShouldSerializeValue(object component) {
            return original.ShouldSerializeValue(component);
        }
        public override void SetValue(object component, object value) {
            original.SetValue(component, value);
        }
        public override Type PropertyType {
            get { return original.PropertyType; }
        }
        public override string DisplayName {
            get {
                var name = base.DisplayName;
                var prop = typeof(RESOURCES).GetProperty(name, BindingFlags.Static | BindingFlags.NonPublic);
                if(prop != null) {
                    return (string)prop.GetValue(null);
                }
                return name;
            }
        }
    }
}


ジェネリクスを利用し、型パラメータにResourcesクラスを指定して使う。
リソースにプロパティ名と同名の文字列を定義していればその値が表示され、定義していなければプロパティ名がそのまま表示されるという寸法だ。

使い方としては、こんな感じになる。

[TypeConverter(typeof(LocalizableConverter<Resources>))]
public class AppProperty {
    [Category("基本情報"), Description("名前です。")]
    public string Name { get; set; }
}

あとは、Properties\Resources.resxに「Name」=「名前」になるように文字列を追加してやれば良い。