既存の型コンバータを置換する - Replace Default TypeConverter

Polymorphic Databinding Solutions - Ayende @ Rahien

この記事自体は、データバインディングで期待されたポリモーフィックな動作を行わないことに対しての解決方法であり、ふむふむなるほどなのだが、私はふと、過去の未解決だった時の日記を思い出した。

派生ImageConverter 作れず
Another ImageConverter

このときは諦めて、ベタに文字列からImageクラスに変換できるかどうかを調べるコードを書いてごまかしていたのだが、Ayendeの記事を見てふと出来るのではないかと思ってやってみた。

  • 新たなTypeConverterを用意する

元々やりたかったことは、例えばDIコンテナでプロパティにインジェクションされた文字列をパスと見立てて、Imageクラスのインスタンスに変換することだが、過去に書いたように、新たな型コンバータを作ること自体は非常に簡単だ。

public class AnotherImageTypeConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext context,
        Type sourceType)
    {
        if (sourceType == typeof(string))
        {
            return true;
        }
        return base.CanConvertFrom(context, sourceType);
    }
    public override object ConvertFrom(ITypeDescriptorContext context,
       CultureInfo culture, object value)
    {
        if (value is string)
        {
            string path = value as string;
            return Image.FromFile(path);
        }
        return base.ConvertFrom(context, culture, value);
    }
}

とりあえずは動くのを確認したかったので、必要最低限の実装しかしていない。文字列からImageクラスの生成もImageクラスのメソッドを使っているだけだ。この型コンバータをシステムに簡単に登録する術があれば話は簡単なのだが、その手は使えない。
Ayendeの記事は、TypeConverterとは関係の無い、拡張したTypeDescriptionProviderをシステムに登録するコードなのだが、この拡張したTypeDescriptionProviderクラスは以下のメソッドをオーバライドしている。

public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
{
   return base.GetTypeDescriptor(typeof(Animal), instance);
}

どうやら拡張したTypeDescriptionProviderは、内部で別のICustomTypeDescriptorを返すことができるらしい。ICustomTypeDescriptorは、ユーザ定義のTypeDescriptorの実装に使用するインタフェースだが、実はこのインタフェースが型コンバータを返すことが出来るのである。
つまりは、型コンバータを用意できたら、その型コンバータを取得する独自のTypeDescriptorを用意して、そのTypeDescriptorを返す、独自のTypeDescriptionProviderを用意すれば良いはずだ。

  • 新たなTypeDescriptorとTypeDescriptionProviderを用意する

ここでも必要最小限の実装しかしていないし、これが正しい実装かも解らないが、取り合えず実装する。

public class ImageTypeDescriptor : CustomTypeDescriptor
{
    private AnotherImageTypeConverter newConverter;
    public ImageTypeDescriptor()
        :base()
    {
        this.newConverter = new AnotherImageTypeConverter();
    }
    public override TypeConverter GetConverter()
    {
        return this.newConverter;
    }
}
public class ImageTypeDescriptionProvider : TypeDescriptionProvider
{
    private ImageTypeDescriptor newDescriptor;
    public ImageTypeDescriptionProvider()
        : base(TypeDescriptor.GetProvider(typeof(Image)))
    {
        this.newDescriptor = new ImageTypeDescriptor();
    }
    public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
    {
        if (objectType == typeof(Image))
        {
            return this.newDescriptor;
        }
        return base.GetTypeDescriptor(objectType, instance);
    }
}

これで準備は完了。既存の型であるImageクラスに対しての型コンバータを置換するには、Ayendeのコードと同様にImageTypeDescriptionProviderをシステムに登録すれば良い。

動作は以下のサンプルで確認してみた。Form1にはPictureBoxを一つ貼り付けてある簡単なものだ。

TypeDescriptionProvider provider = new ImageTypeDescriptionProvider();
TypeDescriptor.AddProvider(provider, typeof(Image));
TypeConverter converter = TypeDescriptor.GetConverter(typeof(Image));
string imgPath = @"c:\temp\image\hogehoge.jpg";
if (converter.CanConvertFrom(typeof(string)))
{
    Image img = converter.ConvertFrom(imgPath) as Image;
    form1.pictureBox1.Image = img;
}
Application.Run(form1);
TypeDescriptor.RemoveProvider(provider, typeof(Image));

これでやっと新しい型コンバータ、AnotherImageTypeConverterによってパス文字列がImageに変換できる。

既存の型コンバータを置き換えてしまうと問題が出る場合があるので、ユーザ定義の型コンバータは使い終わったら登録を解除するべきだろう。最後の行で置換した型コンバータを無効にできる。

TypeDescriptor.RemoveProvider(provider, typeof(Image));

新しい型コンバータが働いているかどうかを確かめたい場合は、この行をTypeDescriptor.AddProviderが書かれている直後に移動してみると良い。TypeConverter::CanConvertFromメソッドが偽を返すはずだ。

ひょんな所から上手くいった訳だが、これがベストな方法かどうかは解らない。ユーザ定義型に対して型コンバータを適用する例(カスタム属性を使う)は枚挙に暇が無いのだが、既存の型の型コンバータを置換してしまう例は見かけないからだ。

てか、こんなことばっかりやってるからドキュメントが進まないんだな。