none
独自コレクションをWebコントロールをPropertyGridで編集したい RRS feed

  • 質問

  • こんにちは。

    以下のクラスのコレクション、PropSetCollectionを持つTextBoxを継承したクラスTextBoxExを作成しました。
    このコレクションはPropSetsというプロパティを介して公開されています。

      [TypeConverter(typeof(PropSetConverter))]
      public class PropSet
      {
        public string A { get; set; }
        public string B { get; set; }
      }

    ゴールはこのPropSetのコレクション、PropSetsというプロパティをVisual Studio上のPropertyGridで編集出来るようにすることです。

    まず、このクラス及びコレクションを文字列に変換するコンバータを作成しました。
    クラスのコンバータ、PropSetConverterではプロパティAとBを「-」で接続した文字列に変換します。PropSetConverterのConvertToメソッドとConvertFromメソッドは以下のように実装しました。

      public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
      {
       if (destinationType == typeof(InstanceDescriptor) && value is PropSet)
       {
        ConstructorInfo info = typeof(PropSet).GetConstructor(new Type[] {typeof(string), typeof(string) });
        PropSet set = (PropSet)value;
    
        return new InstanceDescriptor(info, new object[] { set.A, set.B });
       }
    
       if (destinationType == typeof(string) && value is PropSet)
       {
        PropSet set = (PropSet)value;
    
        return string.Format("{0}-{1}", set.A, set.B);
       }
    
       return base.ConvertTo(context, culture, value, destinationType);
      }
    
      public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
      {
       if (value is string)
       {
        string[] values = value.ToString().Split(new string[] { "-" }, 2, StringSplitOptions.None);
        PropSet set = new PropSet();
    
        set.A = values[0];
        set.B = values[1];
    
        return set;
       }
    
       return base.ConvertFrom(context, culture, value);
      }
    

    コレクションのコンバータ、PropSetCollectionConverterでは各PropSet要素をカンマで列挙した文字列に変換します。
    PropSetCollectionConverterのConvertToメソッドとConvertFromメソッドは以下のように実装しました。

      public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
      {
       if (destinationType == typeof(InstanceDescriptor) && value is PropSetCollection)
       {
        ConstructorInfo info = typeof(Collection<PropSet>).GetConstructor(new Type[] { typeof(IList<PropSet>) });
        IList<PropSet> list = value as IList<PropSet>;
    
        return new InstanceDescriptor(info, new object[] { list });
       }
    
       if (destinationType == typeof(string) && value is PropSetCollection)
       {
        PropSetCollection collection = value as PropSetCollection;
        List<string> items = new List<string>();
    
        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);
      }
    
      public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.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('-');
         PropSet ps = new PropSet();
    
         ps.A = values[0];
         ps.B = values[1];
    
         collection.Add(ps);
        }
    
        return collection;
       }
    
       return base.ConvertFrom(context, culture, value);
      }
    

    TextBoxExを画面に貼り付け、PropertyGridから(A,B)=(a,b)と(A,B)=(c,d)という要素を追加した場合、HTMLソース部のPropSets属性に"a-b,c-d"が付加されるところまでは作ることが出来ましたが、実際に実行すると、「オブジェクト参照にオブジェクトインスタンスが設定されていません。」とパーサーエラーが発生します。

    デザイナ上ではちゃんと表示されており、実行時にブレークポイントを張って追いかけてみましたが原因がわかりません。
    何か不足している処理があるのでしょうか。
    以下が実際に表示されるエラーです。
    パーサー エラー 
    
    説明: この要求の処理に必要なリソースの解析中にエラーが発生しました。以下の解析エラーの詳細を確認し、ソースファイルに変更を加えてください。 
    
    パーサー エラー メッセージ: オブジェクト参照がオブジェクト インスタンスに設定されていません。
    
    ソース エラー: 
    
    行 1: <%@ Page Title="ホーム ページ" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true"
    行 2: CodeBehind="Default.aspx.cs" Inherits="ConverterSample._Default" %>
    行 3: 
    
    ソース ファイル: /default.aspx 行: 1 
    

    どなたかご助力ください。よろしくお願いします。

    • 編集済み Rizun 2010年10月21日 1:30
    2010年10月20日 5:46

回答

  • こちらに書いてある方法を試してみましたら、一応正常に動作するようでした。

    Passing int list as a parameter to a web user control
    http://stackoverflow.com/questions/251439/passing-int-list-as-a-parameter-to-a-web-user-control

    コンストラクタの呼び出し情報を返す代わりに、インスタンス生成メソッドの情報を返す方法になります。
    PropSet に ToString() を実装し、PropSetCollectionConverter への FromString の実装は ConvertFrom の中身を使いました(ConvertFrom では FromString を呼び出すようにしました)。

    ただ、私はリンク先に書かれているエラー発生の理由は理解できてません。
    よくわからないまま、試しに PropSet の配列を引数にとるコンストラクタを実装し、それを呼び出すための情報を渡すように書きなおしてもエラーが発生しました。

    それと、この回避策以外にもっとよい方法が本当はあるんじゃないかと想像するのですが、そもそも私は destinationType が InstanceDescriptor な場合の意義が理解できてません。(--;
    ConvertTo に PropSetCollection のインスタンスが value として渡されるのに、それと等価なインスタンスの作成方法を戻り値にしなければいけないところとか、基本的なことがわかってません。
    Rizun さんのコードを元にして勉強しようと思ってます。

    • 回答としてマーク Rizun 2010年10月26日 6:10
    2010年10月26日 1:46
  • リンク先のコードそのままですが、リンク先がなくなるといけないので一応コードも書かせてもらいます。

    public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
    {
        if (destinationType == typeof(InstanceDescriptor) && value is PropSetCollection)
        {
            //ConstructorInfo info =
            //    typeof(Collection<PropSet>).GetConstructor(new Type[] { typeof(IList<PropSet>) });
            //IList<PropSet> list = value as IList<PropSet>;
            //return new InstanceDescriptor(info, new object[] { list });

            var list = (PropSetCollection)value;
            return new InstanceDescriptor(this.GetType().GetMethod("FromString"),
                new object[] { string.Join(",", list.Select(i => i.ToString()).ToArray()) });
        ・・・
    }

    public static PropSetCollection FromString(string value)
    {
       ・・・
    }

    追記:
    引数用の文字列作成部分は、destinationType が string の場合と同じものでいけますね。
    すると PropSet への ToString の実装も不要ですね。

    • 編集済み TH01 2010年10月26日 1:56 追記
    • 回答としてマーク Rizun 2010年10月26日 6:10
    2010年10月26日 1:52

すべての返信

  • アップされたコードをもっと削って、問題を再現するのに必要最小限にできない

    でしょうか? ちょっと長すぎてついてゆけません(というか、見る気がしません)。

    これが必要最小限ということでしたら失礼しました。

    2010年10月20日 15:36
  • こんにちは。
    ご指摘ありがとうございます。

    コードを削り、ゴールを明確にするよう質問を修正させて頂きました。

    2010年10月21日 1:32
  • で、Webコントロールではどんなふうにプロパティ宣言をして、aspxではどう記述してるんでしょうか。

    プロパティに PersistenceMode.Attribute が要るはずとか


    Kazuhiko Kikuchi
    2010年10月21日 3:07
  • こんにちは、返信ありがとうございます。

    PersistenceMode属性というものがあることを知りませんでした。
    実際に付与して実行してみたところ、PersistenceMode.Attributeでは変わらずにパーサーエラーが発生しました。
    このときのaspxでの記述は以下になります。

    <cc1:TextBoxEx ID="TextBoxEx1" runat="server" PropSets="a-b,c-d"></cc1:TextBoxEx>
    

    また、PersistenceMode.InnerPropertyを設定したところ、正常に実行され、デザイナ上で設定した値が処理内で取得することが出来ました。
    しかし、代わりにデザイナ上で「ハンドルされていない例外が発生しました。」と表示され、テキストボックスが表示されなくなりました。
    このときのaspxでの記述は以下になります。

    <cc1:TextBoxEx ID="TextBoxEx1" runat="server">
    <PropSets>
    <cc1:PropSet A="a" B="b"></cc1:PropSet>
    <cc1:PropSet A="c" B="d"></cc1:PropSet>
    </PropSets>
    </cc1:TextBoxEx>
    

    この属性を指定した場合、コンバータを設定しなくても同様に処理内で値が取得出来ました。
    属性の指定はPropSetCollectionクラスに付与し、TextBoxExのPropSetsプロパティには付与しておりません。
    PropSetsプロパティの方に設定しても変わりませんでした。
    デザイナ上でも正常に描画されるようにするにはどうしたら良いでしょうか。

    なお、上記aspxはデザイナにより自動で出力されたものです。

    ControlPersister及びControlParser辺りが解決の糸口になるのではないかと思い、調べています。

    2010年10月21日 4:46
  • > PersistenceMode属性というものがあることを知りませんでした。

    コードからは具体的にどのようにしたいのか理解できておりませんが、もし
    属性の設定が問題でしたら、以下のページが参考になりませんか?

    Introduction to TypeConverters, Part II: Expandable Object Converter...
    http://www.dotnetjohn.com/articles.aspx?articleid=121

    属性の設定方法については、必要な情報が含まれていると思います。サンプ
    ルを実行してみると、期待通りにプロパティが設定できます。

    Rizun さんのケースとは違うかもしれませんが、基本は同じではないでしょ
    うか?

    2010年10月21日 13:45
  • おはようございます、返信ありがとうございます。

    提示して頂いたページのサンプルにあるHighlightingをさらにコレクションとして設定保持したいというのが目標となります。
    イメージとしましては、同サンプルのHighlightingRadioButtonListのItemsプロパティの編集が一番近いと思います。
    コレクションエディタで独自クラスの要素を追加設定していく感じです。

    しかし、こちらの環境ではこのItemsプロパティにデザイナ上から要素を追加すると、コントロールの描画中に「HighlightingRadioButtonListにはListItemというパブリックプロパティは含まれていません。」とエラーが発生しました。
    ちなみに、新しいプロジェクトを用意して標準のRadioButtonListで同じ操作をしてもエラーは発生しませんでした。

    必要な情報なのに欠けておりました。こちらの環境は以下になります。

    OS: Windows7 64bit
    IDE: Visual Studio 2010 Professional

    サンプルのバージョンの変換においてWebサイトの変換に失敗しております。
    新しいWebサイトプロジェクトを追加し、そこのWebフォームにHighlightingRadioButtonListを配置して上記操作を実行した次第です。

    基本は確かに同じで、単体の独自クラスの設定は行えておりました。
    ただ、これをコレクションとして保持するようにするところで詰まっております。

    2010年10月22日 0:34
  • > イメージとしましては、同サンプルのHighlightingRadioButtonListのItems
    > プロパティの編集が一番近いと思います。
    > コレクションエディタで独自クラスの要素を追加設定していく感じです。

    やっと理解できたような気がします。でも、すみませんが、どのようにすれば
    解決できるのかは分かりません。お役に立てずすみません。


    > しかし、こちらの環境ではこのItemsプロパティにデザイナ上から要素を追加
    > すると、コントロールの描画中に「HighlightingRadioButtonListにはListItem
    > というパブリックプロパティは含まれていません。」とエラーが発生しました。

    Item の設定をしなかったので、気がつきませんでした。カスタムコントロールの
    開始タグと終了タグの間に Highlighting を入れ子にして、さらに ListItem を
    加えるとダメでした。

    紹介したページでは .InnerProperty になってましたが、それではうまくいかな
    いようです。

    PersistenceMode(PersistenceMode.Attribute) にすれば、Highlighting のプロ
    パティ設定は以下のようになって、Item の設定をしても問題なくなります。

    <cc1:HighlightingRadioButtonList
        id="HighlightingRadioButtonList1"
        runat="server"
        ・・・中略・・・
        Highlighting-BackColor="LightGray"
        Highlighting-CSSClass="mtstyle2"
        Highlighting-Enabled="True"
        Highlighting-ForeColor="Black"
        AppendDataBoundItems="True">
        <asp:ListItem>Item-1</asp:ListItem>
        <asp:ListItem>Item-2</asp:ListItem>
        <asp:ListItem>Item-3</asp:ListItem>
        <asp:ListItem>Item-4</asp:ListItem>
        <asp:ListItem>Item-5</asp:ListItem>
    </cc1:HighlightingRadioButtonList>

     

    • 編集済み SurferOnWww 2010年10月22日 14:26 誤記訂正
    2010年10月22日 14:25
  •  こんにちは。

     今回の場合ですと、このHighlightingクラスのコレクションがプロパティで持つ感じになります。
     例にあるようにTypeConverterを継承して独自コンバータを作成し、デザイナ上では正常に表示されるのですが、実際に実行するとパーサーエラーが発生する、と最初に挙げたような問題に直面します。
     このときのPersistenceModeはAttributeです。これをInnerPropertyに変更しTypeConverterを使わずに実装すると、デザイナ上では先に挙げたエラーで正常に表示できませんが、実際に実行するとちゃんと表示され設定した値も取得できます。
     情報不足だったのですが、ハンドルされていない例外の内容は以下のようなものです。

    「'cc1:TextBoxEx' をプロパティ 'PropSets' で設定できませんでした。」

     教えて頂いたサンプルは仰るとおりに修正したところエラーは表示されなくなりました。加えて、Highlightingオブジェクトのプロパティ値も正常に取得できました。
     これをコレクションとして保持した場合、それぞれの要素の値は取得できませんでした。
     コンバータを上手く使えばどうにかできそうな気がするのですが……。

     現在はPersistenceModeをInnerPropertyに設定し、コントロール専用のDesignerを用意し、GetErrorDesignTimeHtmlをオーバーライドしたところで、独自実装のプロパティが例外を発生した場合に限り、GetDesignTimeHtmlの結果を返すようにして、デザイナ上でエラーが表示されないようにしています。
     根本的解決になってないですね(涙

    2010年10月25日 3:03
  • 若干進展があったので掲載します。

    PropSetCollectionConverterのConvertToメソッドにおいて

            ConstructorInfo info = value.GetType().GetConstructor(new Type[] { typeof(IList<PropSet>) });
            IList<PropSet> list = value as IList<PropSet>;
    
            return new InstanceDescriptor(info, new object[] { list });
    

    の箇所で、引数付きのコンストラクタではなく、以下のように引数無しのコンストラクタを使用することでパーサーエラーは出なくなりました。

            ConstructorInfo info = value.GetType().GetConstructor(new Type[] {});
    
            return new InstanceDescriptor(info, new object[] {});
    
    

     ただ、引数として生成したコレクションの要素を設定していないので、実行時のプロパティの値は要素数0となりデザイナ上で設定した値は取得できませんでした。
     パーサーエラーでの内容が「オブジェクト参照がオブジェクト インスタンスに設定されていません。」でした。
     コンストラクタが違うだけで同じオブジェクトが生成されると思うのですが……。
     InstanceDescriptorによって結果生成されるオブジェクトがNULLなのかと思い、InstanceDescriptorオブジェクトを返す前に実際に生成してみました。

            ConstructorInfo info = value.GetType().GetConstructor(new Type[] { typeof(IList<PropSet>) });
            IList<PropSet> list = value as IList<PropSet>;
    
            InstanceDescriptor descriptor = new InstanceDescriptor(info, new object[] { list });
            object obj = descriptor.Invoke();
    
            return descriptor;
    
    

     この処理中のクイックウォッチで、obj is PropSetCollectionの結果がtrueを返すことを確認できました。
     これ以降のどの処理で例外が発生しているのか調査中ですが、未だ解明できていません。
     
    なお、TextBoxExクラスの問題となっている独自クラスコレクションのプロパティにDesignerSerializationVisibility属性は付与しておりません。(Visible)
     これで生成されるaspxは以下になります。

        <cc1:TextBoxEx ID="TextBoxEx1" runat="server" PropSets="a-hog1e,b-hag1e"></cc1:TextBoxEx>
    
    

     また、PropSetCollectionクラスは以下になります。

      [PersistenceMode(PersistenceMode.Attribute)]
      [TypeConverter(typeof(PropSetCollectionConverter))]
      public class PropSetCollection : Collection<PropSet>
      {
        public PropSetCollection()
          : base()
        {
        }
    
        public PropSetCollection(IList<PropSet> list)
          : base(list)
        {
        }
      }
    
    
    2010年10月25日 9:18
  • こちらに書いてある方法を試してみましたら、一応正常に動作するようでした。

    Passing int list as a parameter to a web user control
    http://stackoverflow.com/questions/251439/passing-int-list-as-a-parameter-to-a-web-user-control

    コンストラクタの呼び出し情報を返す代わりに、インスタンス生成メソッドの情報を返す方法になります。
    PropSet に ToString() を実装し、PropSetCollectionConverter への FromString の実装は ConvertFrom の中身を使いました(ConvertFrom では FromString を呼び出すようにしました)。

    ただ、私はリンク先に書かれているエラー発生の理由は理解できてません。
    よくわからないまま、試しに PropSet の配列を引数にとるコンストラクタを実装し、それを呼び出すための情報を渡すように書きなおしてもエラーが発生しました。

    それと、この回避策以外にもっとよい方法が本当はあるんじゃないかと想像するのですが、そもそも私は destinationType が InstanceDescriptor な場合の意義が理解できてません。(--;
    ConvertTo に PropSetCollection のインスタンスが value として渡されるのに、それと等価なインスタンスの作成方法を戻り値にしなければいけないところとか、基本的なことがわかってません。
    Rizun さんのコードを元にして勉強しようと思ってます。

    • 回答としてマーク Rizun 2010年10月26日 6:10
    2010年10月26日 1:46
  • リンク先のコードそのままですが、リンク先がなくなるといけないので一応コードも書かせてもらいます。

    public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
    {
        if (destinationType == typeof(InstanceDescriptor) && value is PropSetCollection)
        {
            //ConstructorInfo info =
            //    typeof(Collection<PropSet>).GetConstructor(new Type[] { typeof(IList<PropSet>) });
            //IList<PropSet> list = value as IList<PropSet>;
            //return new InstanceDescriptor(info, new object[] { list });

            var list = (PropSetCollection)value;
            return new InstanceDescriptor(this.GetType().GetMethod("FromString"),
                new object[] { string.Join(",", list.Select(i => i.ToString()).ToArray()) });
        ・・・
    }

    public static PropSetCollection FromString(string value)
    {
       ・・・
    }

    追記:
    引数用の文字列作成部分は、destinationType が string の場合と同じものでいけますね。
    すると PropSet への ToString の実装も不要ですね。

    • 編集済み TH01 2010年10月26日 1:56 追記
    • 回答としてマーク Rizun 2010年10月26日 6:10
    2010年10月26日 1:52
  •  ありがとうございます、動作しました!

     int型配列に対する Descriptor がないからインスタンスの生成で失敗し、結果例外を吐くみたいですね。
     ConvertTo に value としてオブジェクトが渡ってきているのにその生成方法を返さなければいけないところは私もよく理解できておりません。
     恐らく、生成処理をカプセル化したいのではと推測します。そうなるとそもそも value に渡ってくる オブジェクトはどこで生成されてくるのか……。

     追記にありますように、文字列への変換部分は ConvertTo メソッドの destinationType が string の場合と同じですので、以下のように実装しました。

      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)) });
       }
    ......

     SurferOnWwwさん、kazukさん、TH01さん、本当にありがとうございました!

    • 回答としてマーク Rizun 2010年10月26日 6:10
    • 回答としてマークされていない Rizun 2010年10月26日 6:10
    2010年10月26日 6:10