Custom, Complicated, Dynamic Reflection Solution - C#
Asked Answered
B

2

9

I have many custom classes that I am using that I will explain and post examples of. After Explaining what they all do I will try to clearly describe the conditions under which my bug is happening.

First, I am using a PropertyGrid to display the properties of several different types of objects. Because the default binding of the PropertyGrid was not what as descriptive as I wanted it to be, I created a few custom classes that I will refer to as "Display" classes. These Display classes are constructed by passing in the an object and then creating properties that return nicely formatted strings and descriptions for the public properties (and in some case methods) of the real object that was passed in.

I will demonstrate this with some abbreviated example code:

Here is an example of an object I want to Display in my PropertyGrid:

public class Joint
{
   public Joint(...)
   {...}

   //properties
   public string Name { get; set;}
   public CustomObject CC { get; set;}
   public List<CustomObject> Custom List { get; set;}
}

The string property "Name" displays fine in the PropertyGrid However The CustomObject and List did not display in way that seemed very user friendly to me.

So I attempted to create a solution by writing this class:

public class DisplayJoint
{       

   private Joint _jnt;

   public DisplayJoint(Joint jnt)
   {
      _jnt = jnt;
   }

   //properties
   public string Name {  get { return _jnt.Name; } }

   [TypeConverterAttribute(typeof(ExpandableObjectConverter))]
   public DisplayCustomObject CC {  get { return new DisplayCustomObject(_jnt.CC); } }

   [TypeConverterAttribute(typeof(ExpandableObjectConverter))]
   public List<CustomObject> CustomList { get; set;}
}

As you can see in the code above, I created special DisplayClasses for both my Joint class and my CustomObject class. In my project I have many, many different kinds of objects that require the same kind of overlaping Display Class properties.

Above you can see the Lines I added above the last two properties

[TypeConverterAttribute(typeof(ExpandableObjectConverter))]

This line solves my problem of displaying the CustomObject how I want to in the propertGrid(almost... more on this later). However it does not work the same way for my Custom List property. On the Custom List it expands to show only Count and capacity(The actual properties of the List) It makes sense why this is, but it was not what I wanted. I wanted to see the actual contained object within the list.

enter image description here

So here is my complicated solution, Taken initially from this question:

I have two classes that I am using to dynamically add objects to the propertyGrid bound list in the form of properties. The first(CustomClass) can be downloaded here. It is used to Dynamically create properties. The second class(DisplayIEnumerable) I am using is derived from the first and can be found here.

The DisplayIEnumerable class loops through the list objects and adds a property to itself with the information contained within the each object. A DisplayClass is passed in to define exactly how those objects properties should be represented within the Grid.

Up to this Point everything works great! as evidenced by this picture(picture was not created using the classes provided, Strings are formatted differently in the classes I am using, removed formatting code to help you focus on the relevant code:

enter image description here

Now after that long intro, the real question. Using the techniques above I would like to write a class that can dynamically handle CustomObjects that I have not written unique display classes for. I am intending to leave this code for those using the application for testing so that they can more effectively test without having to have a complete Display Class for every one of my company's CustomObjects. (there are hundreds) Instead, By binding the propertyGrid with the class below, I hope to have all the properties that are lists and CustomObjects that do have corresponding DisplayClasses to be bound in their place.

Here is the class that I have already tried and have a bug with. I have not yet tried implementing the replacement of Lists with my DisplayIEnumerable class yet, I wanted to get the basic functionality working first:

using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Reflection;
using System.Collections;
using System.Windows.Forms;

   internal class DisplayObject : CustomClass<T>
   {
      #region Variables
      protected T _obj;
      #endregion

      #region Constructor
      public DisplayObject(T obj)
      {
         if (obj != null)
         {
            try
            {
               Type currentType = typeof(T);
               foreach (PropertyInfo propertyInfo in currentType.GetProperties())
               {
                  Attribute[] attributes = new Attribute[1];
                  if (propertyInfo.GetType() is IEnumerable)
                     attributes[0] = new TypeConverterAttribute(typeof(ExpandableObjectConverter));
                  else
                     attributes[0] = null;
                  this.Add(new CustomProperty(propertyInfo.Name, propertyInfo, propertyInfo.GetType(), false, true, attributes));
               }
            }
            catch
            {
               MessageBox.Show("Failure!");
            }
         }
      }
      #endregion

      #region Properties
      [Browsable(false)]
      public object Item
      {
         get { return _obj; }
         set { _obj = value; }
      }
      #endregion
   }

When run, The PropertyGrid appears as it should: Before

However, once you click on the Expand arrow, nothing happens, and the arrow disappears: After

What is wrong with the class above that is not wrong with my DisplayIEnumerable class, that causes this variance in behaviour?

I am using the DisplayObject class like this(inside a DisplayClass):

  [TypeConverterAttribute(typeof(ExpandableObjectConverter))]
  public DisplayObject EndJoint { get { if (_member.bcEnd != null) { return new DisplayObject(_member.EndJoint); } else return null; } }

Thanks in advance! I will be very impressed if anyone makes it through this question.

Battlement answered 7/8, 2012 at 0:35 Comment(5)
Why don't you use a standard collection UITypeEditor (the standard form with two columns)?Snowy
Because I will not be able to recursively add this expandable attribute to the properties contained within the properties. I want the user to be able to Drill down as far as they want to completely see the relationships between the different objectsBattlement
The standard collection form includes a property grid so it's also recursive.Snowy
Will I be able to dynamically bind that datagrid to Display classes instead of to the base objects themselves?Battlement
They're all standard property grids, so they support custom TypeDescriptorsSnowy
S
6

You don't have to create special classes to use the property grid. Just decorate the properties with the proper attributes. Here is an example:

Two custom classes:

public class MyObjType1
{
    public int Id { get; set; }
    public string Name { get; set; }

    public override string ToString()
    {
        return Name;
    }
}

public class MyObjType2
{
    public string Reference { get; set; }

    public override string ToString()
    {
        return Reference;
    }
}

Note the ToString is overriden, that's what the property grid uses by default if no TypeConverter is defined for a given type.

One "holder" class that has a collection of custom objects:

public class MyHolder
{
    public MyHolder()
    {
        Objects = new List<object>();
    }

    public string Name { get; set; }

    [TypeConverter(typeof(MyCollectionConverter))]
    public List<object> Objects { get; private set; }
}

Note the custom TypeConverter applied directly to the Objects property. Here is the source:

public class MyCollectionConverter : ExpandableObjectConverter
{
    public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
    {
        IEnumerable enumerable = value as IEnumerable;
        if (enumerable == null)
            return base.GetProperties(context, value, attributes);

        int i = 0;
        List<PropertyDescriptor> list = new List<PropertyDescriptor>();
        foreach (object obj in enumerable)
        {
            MyItemPropertyDescriptor index = new MyItemPropertyDescriptor(i.ToString(), obj);
            list.Add(index);
            i++;
        }
        return new PropertyDescriptorCollection(list.ToArray());
    }

    public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
    {
        if (destinationType != typeof(string))
            return base.ConvertTo(context, culture, value, destinationType);

        IEnumerable enumerable = value as IEnumerable;
        if (enumerable == null)
            return base.ConvertTo(context, culture, value, destinationType);

        StringBuilder sb = new StringBuilder();
        foreach (object obj in enumerable)
        {
            if (sb.Length > 0)
            {
                sb.Append(',');
            }
            sb.AppendFormat("{0}", obj);
        }
        return sb.ToString();
    }
}

Note we override ConvertTo and give it a special string that displays a comma-separated list of objects in the list. The GetProperties is also overriden and uses a special PropertyDescriptor; It adds an ExpandableObjectConverter attribute to sub objects so they can be expanded too:

public class MyItemPropertyDescriptor : PropertyDescriptor
{
    private object _value;

    public MyItemPropertyDescriptor(string name, object value)
        : base(name, new[] { new TypeConverterAttribute(typeof(ExpandableObjectConverter)) })
    {
        _value = value;
    }

    public override bool IsReadOnly
    {
        get { return false; }
    }

    public override object GetValue(object component)
    {
        return _value;
    }

    public override Type PropertyType
    {
        get { return _value == null ? typeof(object) : _value.GetType(); }
    }

    public override bool ShouldSerializeValue(object component)
    {
        return false;
    }

    public override Type ComponentType
    {
        get { return typeof(object); }
    }

    public override bool CanResetValue(object component)
    {
        return false;
    }

    public override void ResetValue(object component)
    {
    }

    public override void SetValue(object component, object value)
    {
    }
}

Now, here is some sample code:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        MyHolder holder = new MyHolder();
        for (int i = 0; i < 3; i++)
        {
            holder.Objects.Add(new MyObjType1 { Id = i, Name = i + "Name" });
        }
        for (int i = 0; i < 3; i++)
        {
            holder.Objects.Add(new MyObjType2 { Reference = "Ref" + i });
        }
        propertyGrid1.SelectedObject = holder;
    }
}

And the result:

enter image description here

Snowy answered 10/8, 2012 at 14:47 Comment(3)
Sorry for not being able to answer your comment a few days ago, I have been travelling and have just made it back to SO, This answer is just what I needed Thanks so much.Battlement
Also, If you don't mind me asking.. You have answered a few of my questions now and I really appreciate the help, But I couldn't help but notice on your profile that with all the questions you have answered, you have not asked a single one!?Battlement
@John - no, I usually find answers to my questions alone (many on SO BTW) :-)Snowy
D
1

Having worked with TypeConverters myself, I can confirm they are a major pain in the bottom parts. You get nada info about what is actually going wrong, only weird output...

Idk if it helps, but maybe it is a problem that you add an empty (null) array to anything that is not an IEnumerable? Try moving the add instruction into the scope of the if (...). I dont think there is any harm in that.

Also, are you certain that (in the last example with the EndJoint) the getter does not return a null pointer? Blank entries smell like null pointers being passed from my experiences.

Drier answered 8/8, 2012 at 15:19 Comment(1)
Def understand what you mean about the poor feedback and TypeConverters, Not sure I understand what you mean about moving the add into the scope of the if. Can you provide some code for a visual illustration? thanksBattlement

© 2022 - 2024 — McMap. All rights reserved.