Mark Roworth
Mark Roworth

Reputation: 566

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.

Upvotes: 1

Views: 1080

Answers (3)

NineBerry
NineBerry

Reputation: 28499

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;

Upvotes: 2

NineBerry
NineBerry

Reputation: 28499

Is it okay if the model already knows what list will be used for a property? If yes, then:

Give your StringListConverter a type parameter. The type specified there will then be responsible for providing the list.

public interface IValueListSupplier
{
    IEnumerable<string> GetValues();
}

 public class StringListConverter<VALUES> : TypeConverter where VALUES : IValueListSupplier, new()
{

    public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
    {
        return true;
    }

    public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
    {
        VALUES values = new VALUES();
        return new StandardValuesCollection(values.GetValues().ToList());
    }
}

You can then create specific types to represent certain lists and use those in your model:

public class SizeValues : IValueListSupplier
{
    public IEnumerable<string> GetValues()
    {
        var values = new List<string>();
        // Or load from database here
        values.Add("XS");
        values.Add("S");
        values.Add("M");
        values.Add("L");
        values.Add("XL");
        return values;
    }
}

public class SupplierValues : IValueListSupplier
{
    public IEnumerable<string> GetValues()
    {
        var values = new List<string>();
        // Or load from database here
        values.Add("Primark");
        values.Add("M&S");
        values.Add("Sports Direct");
        return values;
    }
}

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

    [TypeConverter(typeof(StringListConverter<SizeValues>))]
    public string ClothingSize
    {
        get { return _clothingSize; }
        set { _clothingSize = value; }
    }

    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }

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

}

Upvotes: 0

Simon Mourier
Simon Mourier

Reputation: 138776

ITypeDescriptorContext.Instance Property will contain the instance of the object selected in the property grid (here a Clothing object) and ITypeDescriptorContext.PropertyDescriptor Property will contain the property descriptor (here ClothingSize)

Upvotes: 0

Related Questions