display list of custom objects as a drop-down in the PropertiesGrid

Michael McCarthy picture Michael McCarthy · Mar 2, 2011 · Viewed 18.3k times · Source

I want to take an object, let's say this object:

public class BenchmarkList
{
    public string ListName { get; set; }
    public IList<Benchmark> Benchmarks { get; set; }
}

and have that object display its ListName as the "name" part of the PropertiesGrid ("Benchmark" would be good), and for the "value" part of the PropertyGrid, to have a drop-down list of the IList<> of Benchmarks:

here is the Benchmark object

public class Benchmark
{
    public int ID {get; set;}
    public string Name { get; set; }
    public Type Type { get; set; }
}

I would want the drop-down to show the Name property of the Benchmark for what the users can see. Here is a visual example:

enter image description here

So, essentially, I'm trying to get a collection of Benchmark objects into a drop-down list, and those objects should show their Name property as the value in the drop-down.

I've read other articles on using the PropertiesGrid, including THIS and THIS, but they are more complex than what I'm trying to do.

I usually work on server-side stuff, and don't deal with UI via WebForms or WinForms, so this PropertiesGrid is really taking me for a ride...

I do know my solution lies in implementing "ICustomTypeDescriptor", which will allow me to tell the PropertiesGrid what values it should be displaying regardless of the properties of the object to which I want to bind into the drop-down list, but I'm just not sure how or where to implement it.

Any pointers/help would be much appreciated.

Thanks, Mike

UPDATE:

Okay, so I'm changing the details around a little. I was going overboard before with the objects I thought should be involved, so here is my new approach.

I have an object called Analytic. This is the object that should be bound to the PropertiesGrid. Now, if I expose a property that is of an enum type, PropertiesGrid will take care of the drop-down list for me, which is very nice of it. If I expose a property that is a collection of a custom type, PropertiesGrid is not so nice...

Here is the code for Analytic, the object I want to bind to the PropertiesGrid:

public class Analytic
{ 
    public enum Period { Daily, Monthly, Quarterly, Yearly };
    public Analytic()
    {
        this.Benchmark = new List<IBenchmark>();
    }
    public List<IBenchmark> Benchmark { get; set; }
    public Period Periods { get; set; }
    public void AddBenchmark(IBenchmark benchmark)
    {
        if (!this.Benchmark.Contains(benchmark))
        {
            this.Benchmark.Add(benchmark);
        }
    }
}

Here is a short example of two objects that implement the IBenchmark interface:

public class Vehicle : IBenchmark
{
    public Vehicle()
    {
        this.ID = "00000000-0000-0000-0000-000000000000";
        this.Type = this.GetType();
        this.Name = "Vehicle Name";
    }

    public string ID {get;set;}
    public Type Type {get;set;}
    public string Name {get;set;}
}

public class PrimaryBenchmark : IBenchmark
{
    public PrimaryBenchmark()
    {
        this.ID = "PrimaryBenchmark";
        this.Type = this.GetType();
        this.Name = "Primary Benchmark";
    }

    public string ID {get;set;}
    public Type Type {get;set;}
    public string Name {get;set;}
}

These two objects will be added to the Analytic object's Benchmark List collection in the WinForms code:

private void Form1_Load(object sender, EventArgs e)
{
    Analytic analytic = new Analytic();
    analytic.AddBenchmark(new PrimaryBenchmark());
    analytic.AddBenchmark(new Vehicle());
    propertyGrid1.SelectedObject = analytic;
}

Here is a screen-grab of the output in the PropertiesGrid. Note that the property exposed as an enum gets a nice drop-down list with no work, but the property exposed as an of List on gets a value of (Collection). When you click on (Collection), you get the Collection editor and then can see each object, and their respective properties:

enter image description here

This is not what I'm looking for. Like in my first screen grab in this post, I'm trying to render the property Benchmark collection of List as a drop-down list that shows the object's name property as the text of what can be displayed...

Thanks

Answer

Simon Mourier picture Simon Mourier · Mar 3, 2011

In general, a drop down list in a property grid is used for setting the value of a property, from a given list. Here that means you should better have a property like "Benchmark" of type IBenchmark and a possible list of IBenchmark somewhere else. I have taken the liberty of changing your Analytic class like this:

public class Analytic
{
    public enum Period { Daily, Monthly, Quarterly, Yearly };
    public Analytic()
    {
        this.Benchmarks = new List<IBenchmark>();
    }

    // define a custom UI type editor so we can display our list of benchmark
    [Editor(typeof(BenchmarkTypeEditor), typeof(UITypeEditor))]
    public IBenchmark Benchmark { get; set; }

    [Browsable(false)] // don't show in the property grid        
    public List<IBenchmark> Benchmarks { get; private set; }

    public Period Periods { get; set; }
    public void AddBenchmark(IBenchmark benchmark)
    {
        if (!this.Benchmarks.Contains(benchmark))
        {
            this.Benchmarks.Add(benchmark);
        }
    }
}

What you need now is not an ICustomTypeDescriptor, but instead a TypeConverter an an UITypeEditor. You need to decorate the Benchmark property with the UITypeEditor (as above) and the IBenchmark interface with the TypeConverter like this:

// use a custom type converter.
// it can be set on an interface so we don't have to redefine it for all deriving classes
[TypeConverter(typeof(BenchmarkTypeConverter))]
public interface IBenchmark
{
    string ID { get; set; }
    Type Type { get; set; }
    string Name { get; set; }
}

Here is a sample TypeConverter implementation:

// this defines a custom type converter to convert from an IBenchmark to a string
// used by the property grid to display item when non edited
public class BenchmarkTypeConverter : TypeConverter
{
    public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
    {
        // we only know how to convert from to a string
        return typeof(string) == destinationType;
    }

    public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
    {
        if (typeof(string) == destinationType)
        {
            // just use the benchmark name
            IBenchmark benchmark = value as IBenchmark;
            if (benchmark != null)
                return benchmark.Name;
        }
        return "(none)";
    }
}

And here is a sample UITypeEditor implementation:

// this defines a custom UI type editor to display a list of possible benchmarks
// used by the property grid to display item in edit mode
public class BenchmarkTypeEditor : UITypeEditor
{
    private IWindowsFormsEditorService _editorService;

    public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
    {
        // drop down mode (we'll host a listbox in the drop down)
        return UITypeEditorEditStyle.DropDown;
    }

    public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
    {
        _editorService = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));

        // use a list box
        ListBox lb = new ListBox();
        lb.SelectionMode = SelectionMode.One;
        lb.SelectedValueChanged += OnListBoxSelectedValueChanged;

        // use the IBenchmark.Name property for list box display
        lb.DisplayMember = "Name";

        // get the analytic object from context
        // this is how we get the list of possible benchmarks
        Analytic analytic = (Analytic)context.Instance;
        foreach (IBenchmark benchmark in analytic.Benchmarks)
        {
            // we store benchmarks objects directly in the listbox
            int index = lb.Items.Add(benchmark);
            if (benchmark.Equals(value))
            {
                lb.SelectedIndex = index;
            }
        }

        // show this model stuff
        _editorService.DropDownControl(lb);
        if (lb.SelectedItem == null) // no selection, return the passed-in value as is
            return value;

        return lb.SelectedItem;
    }

    private void OnListBoxSelectedValueChanged(object sender, EventArgs e)
    {
        // close the drop down as soon as something is clicked
        _editorService.CloseDropDown();
    }
}