スポンサーリンク

2010年11月15日月曜日

PropertyGrid で独自クラスのコレクションを編集する方法~その2

 前回のお話の続き。

 今回はいよいよCollectionConverterクラスを継承したコンバータを作成したいと思います。

 具体的には、aspx内部の記述(string)からPropSetCollectionインスタンスへの変換を行うCanConvertFromとConvertFromの実装、その逆のPropSetCollectionインスタンスからaspx内部の記述(string)への変換を行うCanConvertToとConvertToの実装です。


 まず、CanConvertFromとConvertFromから。

 CanConvertFromでは、aspx内部の記述、つまりstringであれば変換可能であることを示す必要があります。stringでも実際に変換可能かどうかはわかりませんが、その辺りはConvertFromで実装します。

 aspx内部の記述からと言ってますが、簡単に言えばstringからの変換です。CanConvertFromでは元のデータ型がstringであればtrueを返すようにします。また、それ以外であれば基底のメソッドにお任せします。

 ここでは単純に文字列であることのみを判定条件としてますが、正規表現を絡めて決まった書式であるかどうかも検証した方がいいと思います。
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
    if (sourceType == typeof(string))
    {
        return true;
    }

    return base.CanConvertFrom(context, sourceType);
}
次に実際に変換を行うConvertFromメソッドを実装します。

 受け取った値が文字列である場合に変換処理を行います。前回の記事にあるように書式は決まっているので、その書式に合わせて分解しインスタンスを生成していきます。
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
    if (value is string)
    {
        string[] sets = ((string)value).Split(',');
        PropSetCollection collection = new PropSetCollection();

        foreach (string set in sets)
        {
            string[] values = set.Split('-');
            collection.Add(new PropSet(values[0], values[1]));
        }

        return collection;
    }

    return base.ConvertFrom(context, culture, value);
}
ここでも受け取ったデータが文字列じゃなかったら基底の変換処理にお任せしています。

 では、逆変換の処理を実装していきます。最初はCanConvertToメソッドから。

 変換先のデータ型が文字列のときにtrueを返すようにします。
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
    if (destinationType == typeof(string))
    {
        return true;
    }

    return base.CanConvertTo(context, destinationType);
}
もう同様ですが、変換先が文字列でなかったら基底のメソッドを実行します。

 これで変換先が文字列の場合にConvertToメソッドが実行されるようになるので、実際の変換処理をConvertToメソッドをオーバーライドして記述します。
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
    if (destinationType == typeof(string) && value is PropSetCollection)
    {
        PropSetCollection collection = value as PropSetCollection;
        List items = new List();

        foreach (PropSet set in collection)
        {
            items.Add(string.Format("{0}-{1}", set.A, set.B));
        }
        return string.Join(",", items.ToArray());
    }

    return base.ConvertTo(context, culture, value, destinationType);
}
変換先が文字列であることと合わせて、渡された値がPropSetCollectionのインスタンスであることも確認しています。そうでなかった場合は(以下略

 コレクションの各要素を参照し、決めた書式に合わせて文字列を生成しています。

 これで上手く動くか、というと実は動きません。ここまでの実装はデザイナ上での話です。

 詳しい内部実装はわかりませんが、実処理上ではCanConvertToメソッド及びConvertToメソッドでさらに、PropSetCollectionインスタンスからPropSetCollectionインスタンスを作成するために必要な情報を持つInstanceDescriptorに変換する必要があります。

 変換したいデータ型のインスタンスを既に持ちながら、どうしてそのデータ型のインスタンス生成に必要な情報が必要となるのか。そのままそれ使えばいいじゃん。

 と、疑問に思いつつも要求されているのでその部分の実装をしていきます。

 まずはCanConvertToメソッドに追記。
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
    if (destinationType == typeof(InstanceDescriptor))
    {
        return true;
    }

    if (destinationType == typeof(string))
    {
        return true;
    }

    return base.CanConvertTo(context, destinationType);
}
変換先のデータ型がInstanceDescriptorの場合もtrueを返すようにしています。

 次にConvertToメソッドに追記。
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
    if (destinationType == typeof(InstanceDescriptor) && value is PropSetCollection)
    {
        PropSetCollection collection = value as PropSetCollection;
        ConstructorInfo info = collection.GetType().GetConstructor(new Type[] { typeof(IList) });

        return new InstanceDescriptor(info, new object[] { collection }, true);
    }

    if (destinationType == typeof(string) && value is PropSetCollection)
    {
        PropSetCollection collection = value as PropSetCollection;
        List items = new List();

        foreach (PropSet set in collection)
        {
            items.Add(string.Format("{0}-{1}", set.A, set.B));
        }

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

    return base.ConvertTo(context, culture, value, destinationType);
}
InstanceDescriptorを生成するとき、aspx内部の記述から生成されたPropSetオブジェクトも入った状態でないと意味がないので、PropSetのリストを受け取るコンストラクタを指定し、要素オブジェクトを持つコレクションを渡します。

 とりあえずこれで完成、いざ実行!

 してみると、パーサーエラーなるものが発生します。その原因がNullReferenceException。いったいどこで?!

 デバッグ実行して確認してみても、InstanceDescriptorを生成する過程でNull参照を行っている箇所はありません。

 ここで相当ハマりました。ハマって解決できなかったのでMSDNフォーラムで質問してみたところ、PropSetCollectionのインスタンスを生成する静的メソッドを用意し、そのメソッド情報を使ってInstanceDescriptorを生成することで回避できるようです。

 というわけで、ConvertToメソッドを修正します。
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
    if (destinationType == typeof(InstanceDescriptor) && value is PropSetCollection)
    {
        MethodInfo method = this.GetType().GetMethod("FromString");

        return new InstanceDescriptor(method, new object[] { this.ConvertTo(context, culture, value, typeof(string)) });
    }

    if (destinationType == typeof(string) && value is PropSetCollection)
    {
        PropSetCollection collection = value as PropSetCollection;
        List items = new List();

        foreach (PropSet set in collection)
        {
            items.Add(string.Format("{0}-{1}", set.A, set.B));
        }

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

    return base.ConvertTo(context, culture, value, destinationType);
}
そして、PropSetCollectionのインスタンスを生成するメソッドFromStringを追加します。
public static PropSetCollection FromString(string value)
{
    string[] sets = ((string)value).Split(',');
    PropSetCollection collection = new PropSetCollection();

    foreach (string set in sets)
    {
        string[] values = set.Split('-');

        collection.Add(new PropSet(values[0], values[1]));
    }

    return collection;
}
中身はまんまConvertFrom……ですが、ConvertFromのメソッド情報からではだめでした。InstanceDescriptorで使用するメソッド情報は静的でなければならないようです。

 PropSetCollectionインスタンスを持ってるのに一度文字列に変換し、そこからInstanceDescriptorを使って再度PropSetCollectionを構築する?

 二度手間でしかないような……と思いましたが、どうやらConvertToで受け取ったPropSetCollectionのインスタンスには寿命のようなものがあるっぽく、それを使ってInstanceDescriptorからインスタンスを生成しようとしたときには既にオブジェクトが消えてしまうようです。

 少なくとも、コンバータ内部で生成したインスタンスはそのまま実処理へは持っていけないみたい。詳しい内部実装はわからないけど何となく納得。ちょろっと調査してみただけの結果の推測です。間違ってたらゴメンナサイ。

 なので、文字列の状態を介して再生成する必要があります。再生成できて永続化できるものであれば文字列じゃなくてもいいかも。ぱっと思いつきませんが。

 とりあえず、要素生成のタイミングをコンバータの外に出してあげれば良さそうです。ですので、PropSetCollectionのコンストラクタに書式化された文字列を受け取るようにして、コンストラクタ内でConvertFromまたはFromStringと同様の処理で要素を生成するようにし、そのコンストラクタ情報を使ってInstanceDescriptorを生成しても動作します。

 例としては以下のような感じ。
public class PropSetCollection : Collection
{
    public PropSetCollection()
    {
    }

    public PropSetCollection(IList list)
    {
    }

    public PropSetCollection(string format)
    {
        string[] sets = format.Split(',');

        foreach (string set in sets)
        {
            string[] values = set.Split('-');

            this.Add(new PropSet(values[0], values[1]));
        }
    }
}

public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
    if (destinationType == typeof(InstanceDescriptor) && value is PropSetCollection)
    {
        PropSetCollection collection = value as PropSetCollection;
        ConstructorInfo info = collection.GetType().GetConstructor(new Type[] { typeof(string) });

        return new InstanceDescriptor(info, new object[] { this.ConvertTo(context, culture, value, typeof(string)) }, true);
    }

    if (destinationType == typeof(string) && value is PropSetCollection)
    {
        PropSetCollection collection = value as PropSetCollection;
        List items = new List();

        foreach (PropSet set in collection)
        {
            items.Add(string.Format("{0}-{1}", set.A, set.B));
        }

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

    return base.ConvertTo(context, culture, value, destinationType);
}
どっちもあまり綺麗じゃないような気がしてもやもやしますが……。とりあえず動いたので良しとしましょう。

 あと、PropSetクラスのToStringメソッドをオーバーライドすれば、PropertyGridから起動されたコレクションエディタで表示される要素の表示を変えることができます。実装如何ではConvertToやFromStringでも使えますね。

0 件のコメント:

コメントを投稿