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

18,682

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();
    }
}
Share:
18,682
Michael McCarthy
Author by

Michael McCarthy

Michael McCarthy is a .NET developer with almost 20 years building web-based, business applications. He’s worked in the loan industry, the financial industry and the pharmaceutical industry. He currently works as a Senior C# Developer at ALMAC, where he just finished a two year, green-field project re-write of their clinical trial software using SOA, DDD and TDD. Over the past five years, he’s switched his efforts from “n-tier” development to utilizing an SOA-based, asynchronous and durable approach to building business software powered by NServiceBus. In those years, he’s transformed into a TDD advocate, has embraced the SOLID principles and has come to rely on FakeItEasy for its ease of use and flexible API while writing his unit tests. In his spare time, he loves keeping up to date on all things NServiceBus, DDD, MVC and TDD. Publications: FakeItEasy Succinctly (free PDF download) http://www.syncfusion.com/resources/techportal/details/ebooks/fakeiteasy Published 9/3/2015 Assisted in proofing rough drafts of Implementing Domain Driven Design (the “Red Book”) by Vaughn Vernon. Mentioned in Acknowledgement section: http://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577 Blog: http://www.michaelgmccarthy.com/ LinkedIn: https://www.linkedin.com/in/michaelgmccarthy GitHub: https://github.com/mgmccarthy

Updated on June 21, 2022

Comments

  • Michael McCarthy
    Michael McCarthy almost 2 years

    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

  • Michael McCarthy
    Michael McCarthy about 13 years
    Simon, everything works great! I understand the BenchmarkTypeConverter class, but the BenchmarkTypeEdit class remains a mystery to me. It looks like whenever you click on the drop-down, the whole list has to rebuild itself and then choose the new value. I assume this is how the PropertyGrid must work "behind the scenes", and that's why doing something that is very straight-forward like can take a good amount of code. Either way, thanks so much for extending a helping hand! I'm much further along then I was before. Some good Karma coming your way! Mike
  • Michael McCarthy
    Michael McCarthy about 13 years
    Some other great resources to explain what Simon did here. Again Simon, thanks so much! msdn.microsoft.com/en-us/library/ms171839.aspx msdn.microsoft.com/en-us/library/ms171840.aspx
  • Onur Omer
    Onur Omer over 7 years
    Simon you've explained the concept very well and thank you for a very good and simple working example :) Cheers!
  • jstuardo
    jstuardo about 6 years
    How to make the width of ListBox to be the width of the property grid? I have done it using your code, but ListBox displays with Width of 120 px.
  • Simon Mourier
    Simon Mourier about 6 years
    @jstuardo - If listbox size is unspecified, it should take the width of the property grid's value column (with a minimum size if the grid is small). Otherwise you can set it's width manually. Or ask another question.
  • jstuardo
    jstuardo about 6 years
    listbox size is unspecified. My listbox only has this in EditValue method: _listBox.Height = _listBox.PreferredHeight;