Search code examples
c#propertygridtypeconverter

General purpose dynamic list for a PropertyGrid in C#


Have scoured the net for how to do this, I've managed to scrape together a minimal working example, but I don't understand quite how it works. To replicate, it has a single form with a property grid on (Form1, propertyGrid1). There is an instance of an object of class Clothing, which is assigned as the SelectedObject of the PropertyGrid. There are two properties which require lists that are only known at runtime. They are to be returned by a generic class: StringListConverter.

So, code:

Form1.cs:

    public partial class Form1 : Form
    {
        Clothing obj = new Clothing();
        
        public Form1()
        {
            InitializeComponent();
            propertyGrid1.SelectedObject = obj;
        }
    }

Clothing.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using System.Reflection;
using System.ComponentModel;

namespace PropertyGrid2
{
    public class Clothing
    {
        private string _name = "Shirt";
        private string _clothingSize = "M";
        private string _supplier = "Primark";

        [TypeConverter(typeof(StringListConverter))]
        public string ClothingSize
        {
            get { return _clothingSize; }
            set { _clothingSize = value; }
        }
        
        public string Name
        {
            get { return _name; }
            set { _name = value; }
        }

        [TypeConverter(typeof(StringListConverter))]
        public string Supplier
        {
            get { return _supplier; }
            set { _supplier = value; }
        }
        
    }
}

StringListConverter.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using System.ComponentModel;

namespace PropertyGrid2
{
    public class StringListConverter : TypeConverter
    {
        private List<string> _sizes;
        private List<string> _suppliers;

        public StringListConverter()
            : base()
        {
            _sizes = new List<string>();
            _sizes.Add("XS");
            _sizes.Add("S");
            _sizes.Add("M");
            _sizes.Add("L");
            _sizes.Add("XL");

            _suppliers = new List<string>();
            _suppliers.Add("Primark");
            _suppliers.Add("M&S");
            _suppliers.Add("Sports Direct");
        }
        
        public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
        {
            return true;
        }
        
        public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
        {
            switch (context.PropertyDescriptor.Name)
            {
                case "ClothingSize": return new StandardValuesCollection(_sizes);
                case "Supplier": return new StandardValuesCollection(_suppliers);
                default: throw new IndexOutOfRangeException();
            }
        }
    }
}

In the example shown above, I have to instantiate the contents of _sizes and _suppliers within the constructor of StringListConverter. However, I've very much like to add methods Add, Count, Items, Remove so that it is generic and re-usable for multiple properties on the same object at the same time. I'd like to create one instance of the class per list and load up the items for that list (hence the name StringListConverter). What I'm currently having to do above is load up multiple lists in the class and for it to then understand the object that is picking items from it for its property.

For example, if I have additional properties on Clothing like "Fit" or "PairedItem", I can use the same class, create instances of it and populate them with the appropriate list and then attach them to the appropriate properties.

So, the question I have is:

How can I make StringListConverter so that it is truely generic, contain just one list, supply different instances of it for different properties, and get rid of that switch statement that breaks encapsulation? It shouldn't need to know where it is being called from.


Solution

  • Another approach is to have your StringListConverter allow business code to register a list for a certain property of a certain class like this:

    public class StringListConverter : TypeConverter 
    {
        /// <summary>
        /// Dictionary that maps a combination of type and property name to a list of strings
        /// </summary>
        private static Dictionary<(Type type, string propertyName), IEnumerable<string>> _lists = new Dictionary<(Type type, string propertyName), IEnumerable<string>>();
    
        public static void RegisterValuesForProperty(Type type, string propertyName, IEnumerable<string> list)
        {
            _lists[(type, propertyName)] = list;
        }
    
        public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
        {
            return true;
        }
    
        public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
        {
            if (_lists.TryGetValue((context.PropertyDescriptor.ComponentType, context.PropertyDescriptor.Name), out var list))
            {
                return new StandardValuesCollection(list.ToList());
            }
            else
            {
                throw new Exception("Unknown property " + context.PropertyDescriptor.ComponentType + " " + context.PropertyDescriptor.Name);
            }
    
            
        }
    }
    

    Usage:

    var values = new List<string>();
    values.Add("XS");
    values.Add("S");
    values.Add("M");
    values.Add("L");
    values.Add("XL");
    StringListConverter.RegisterValuesForProperty(typeof(Clothing), nameof(Clothing.ClothingSize), values);
    
    values = new List<string>();
    values.Add("Primark");
    values.Add("M&S");
    values.Add("Sports Direct");
    StringListConverter.RegisterValuesForProperty(typeof(Clothing), nameof(Clothing.Supplier), values);
    
    Clothing obj = new Clothing();
    propertyGrid1.SelectedObject = obj;