12

I want to pass an int list (List) as a declarative property to a web user control like this:

<UC:MyControl runat="server" ModuleIds="1,2,3" />

I created a TypeConverter to do this:

public class IntListConverter : System.ComponentModel.TypeConverter
{
    public override bool CanConvertFrom(
           System.ComponentModel.ITypeDescriptorContext context, 
           Type sourceType)
    {
        if (sourceType == typeof(string)) return true;
        return base.CanConvertFrom(context, sourceType);
    }
    public override object ConvertFrom(
      System.ComponentModel.ITypeDescriptorContext context, 
      System.Globalization.CultureInfo culture, object value)
    {
        if (value is string)
        {
            string[] v = ((string)value).Split(
                new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
            List<int> list = new List<int>();
            foreach (string s in vals)
            {
                list.Add(Convert.ToInt32(s));
            }
            return list
        }
        return base.ConvertFrom(context, culture, value);
    }
    public override bool CanConvertTo(ITypeDescriptorContext context,
      Type destinationType)
    {
        if (destinationType == typeof(InstanceDescriptor)) return true;
        return base.CanConvertTo(context, destinationType);
    }
    public override object ConvertTo(ITypeDescriptorContext context,
      System.Globalization.CultureInfo culture, object value, Type destinationType)
    {
        if (destinationType == typeof(InstanceDescriptor) && value is List<int>)
        {
            List<int> list = (List<int>)value;
            ConstructorInfo construcor = typeof(List<int>).GetConstructor(new Type[] { typeof(IEnumerable<int>) });
            InstanceDescriptor id = new InstanceDescriptor(construcor, new object[] { list.ToArray() });
            return id;
        }
        return base.ConvertTo(context, culture, value, destinationType);
    }
}

And then added the attribute to my property:

[TypeConverter(typeof(IntListConverter))]
public List<int> ModuleIds
{
    get { ... }; set { ... };
}

But I get this error at runtime:

Unable to generate code for a value of type 'System.Collections.Generic.List'1[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]'. This error occurred while trying to generate the property value for ModuleIds.

My question is similar to one found here, but the solution does not solve my problem:

Update: I found a page which solved the first problem. I updated the code above to show my fixes. The added code is the CanConvertTo and ConvertTo methods. Now I get a different error.:

Object reference not set to an instance of an object.

This error seems to be indirectly caused by something in the ConvertTo method.

Community
  • 1
  • 1
Kevin Albrecht
  • 6,974
  • 7
  • 44
  • 56

8 Answers8

10

After hooking a debugger into Cassini, I see that the null ref is actually coming from System.Web.Compilation.CodeDomUtility.GenerateExpressionForValue, which is basically trying to get an expression for the int[] array you pass into the List constructor. Since there's no type descriptor for the int[] array, it fails (and throws a null ref in the process, instead of the "can't generate property set exception" that it should).

I can't figure out a built in way of getting a serializable value into a List<int>, so I just used a static method:

class IntListConverter : TypeConverter {
    public static List<int> FromString(string value) {
       return new List<int>(
          value
           .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
           .Select(s => Convert.ToInt32(s))
       );
    }

    public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) {
        if (destinationType == typeof(InstanceDescriptor)) {
            List<int> list = (List<int>)value;
            return new InstanceDescriptor(this.GetType().GetMethod("FromString"),
                new object[] { string.Join(",", list.Select(i => i.ToString()).ToArray()) }
            );
        }
        return base.ConvertTo(context, culture, value, destinationType);
    }
}
Mark Brackett
  • 84,552
  • 17
  • 108
  • 152
  • 1
    +1 For this answer as well. A working fix to a very obscure problem. I had the same issue as the OP other than using a `List` instead of `List`. Solution worked perfectly with a few minor changes to account for string vs. int. – KP. May 24 '11 at 15:51
  • Thank you for this answer. Now I understand: The page builder is able to emit a "1,2,3" expression but can't emit a new List{"1","2","3"} expression. Is there a way to instruct the builder to emit such an expression? – Ashraf Sabry Dec 18 '13 at 14:06
  • I didn't like the idea of writing the value as a string like "1,2,3" in the generated control class file because it will be deserialized on each request, so I wrote a ControlBuilder for my expression to initialize the collection then adds every value by calling the Add method – Ashraf Sabry Dec 19 '13 at 11:17
1

WHile I can't say I have any particular experience with this error, other sources indicate that you need to add a conversion to the type InstanceDescriptor. check out:

http://weblogs.asp.net/bleroy/archive/2005/04/28/405013.aspx

Which provides an explanation of the reasons or alternatively:

http://forums.asp.net/p/1191839/2052438.aspx#2052438

Which provides example code similar to yours.

Brian B.
  • 1,779
  • 11
  • 10
1

I solved something simular by creating 2 properties:

public List<int> ModuleIDs { get .... set ... }
public string ModuleIDstring { get ... set ... }

The ModuleIDstring converts its value set to a list and sets the ModuleIDs property.

This will also make the ModuleIDs usable from a PropertyGrid etc.

Ok, not the best, typesafe solution, but for me it works.

GvS
  • 52,015
  • 16
  • 101
  • 139
  • This is probably the solution we will end up going with, but it is still strange that we can't figure out how to fix the actual error. – Kevin Albrecht Oct 31 '08 at 16:54
0

pass the list from the code behind...

aspx:

<UC:MyControl id="uc" runat="server" />

code-behind:

List<int> list = new List<int>();
list.add(1);
list.add(2);
list.add(3);

uc.ModuleIds = list;
craigmoliver
  • 6,499
  • 12
  • 49
  • 90
0

The way I normally do this is to make the property wrap the ViewState collection. Your way looks better if it can be made to work, but this will get the job done:

public IList<int> ModuleIds
{
   get
   {
       string moduleIds = Convert.ToString(ViewState["ModuleIds"])

       IList<int> list = new Collection<int>();

       foreach(string moduleId in moduleIds.split(","))
       {
          list.Add(Convert.ToInt32(moduleId));
       }

      return list;
   }
}
Simon Johnson
  • 7,802
  • 5
  • 37
  • 49
0

I believe the problem is the set{}. The type converter want to change the List<int> back into a string, but CanConvertFrom() fails for List<int>.

James Curran
  • 101,701
  • 37
  • 181
  • 258
0

You can pass it into a string and split on comma to populate a private variable. Does not have the nicety of attribution, but will work.

private List<int> modules;
public string ModuleIds
{
  set{
    if (!string.IsNullOrEmpty(value))
    {
    if (modules == null) modules = new List<int>();
    var ids = value.Split(new []{','});
    if (ids.Length>0)
      foreach (var id in ids)
        modules.Add((int.Parse(id)));
    }
}
jkind
  • 9
  • 1
0

I think that you're best option is to make your usercontrol have a DataSource-style property.

You take the property as an object and then do some type checking against IList/ IEnumerable/ etc to make sure that it is correct.

Aaron Powell
  • 24,927
  • 18
  • 98
  • 150