Leo
Leo

Reputation: 1

ComboBox on a PropertyGrid

I'm trying to show a ComboBox on a PropertyGrid. I don't mean a dropdown string list because I need the index of the selected item associated with the description. This is easy to obtain with a ComboBox control on a Form but I can't get the same result by publishing a ComboBox type property in a PropertyGrid.

On My code below I'm trying to get a ComboBox on a PropertyGrid.

 public class Wrap {
    private ComboBox _Combo = new ComboBox();
    public Wrap() {

      _Combo.SelectedIndexChanged += new System.EventHandler(Combo_SelectedIndexChanged);

      _Combo.Items.Add(new Item() { Text = "Text1", Value = "Value1" });
      _Combo.Items.Add(new Item() { Text = "Text2", Value = "Value2" });

      _Combo.DisplayMember = "Text";
      _Combo.ValueMember = "Value";
      _Combo.SelectedIndex = 1;
    }

    private void Combo_SelectedIndexChanged(object sender, EventArgs e) {
      ComboBox cb = sender as ComboBox;
      int i = cb.SelectedIndex;
      if (i < 0)
        return;

      Item it = cb.Items[i] as Item;
      string s = it.Value;
    }

    public ComboBox Combo {
      get { return _Combo; }
      set {
        // Temporary test to avoid "null" (the only selection possible from propertygrid)
        if (value == null)
          return;
        _Combo = value;
      }
    }
  }

  public class Item {
    public string Value { set; get; }
    public string Text { set; get; }
  }

Upvotes: 0

Views: 194

Answers (1)

Gy&#246;rgy Kőszeg
Gy&#246;rgy Kőszeg

Reputation: 18023

Assuming this is a WinForms PropertyGrid you don't need to explicitly use a ComboBox control: a custom TypeConverter handles everything for you.

Please also note that the DisplayMember/ValueMember distinction for a ComboBox is typically needed only when you need to store some primitive underlying value (eg. in a database) to represent some more high level instance in your actual model but in a PropertyGrid you don't need that. You can directly use your high level custom type in the grid. Still, you can optionally parse the low-level "value member" if you want, as it is demonstrated in the following example.

1. The class bound to the PropertyGrid

You didn't provide an example so I made up one. Note the [TypeConverter(...)] attribute above the property or the type itself

public class MyClassToEditInAGrid
{
    // some regular properties here
    public string StringProp { get; set; }
    public int IntProp { get; set; }

    // [...]

    // My special property with the 'ComboBox'.
    // You can omit the TypeConverter here if it is defined globally for the type
    [TypeConverter(typeof(ItemConverter))]
    [Description("Either select an item or type the corresponding text or underlying value")]
    public Item MySelectableProperty { get; set; }
}

Please note that you can define the type converter for your Item "globally" as well:

// You can define the type converter also here so it is used by default for Item.
// Or, you can indicate it just in MyClassToEditInAGrid for the property as above.
[TypeConverter(typeof(ItemConverter))]
public class Item
{
    public string Value { set; get; }
    public string Text { set; get; }

    public override string ToString() => Text;
}
2. The type converter
public class ItemConverter : TypeConverter
{
    // The selectable items
    private static readonly Item[] _items =
    {
        new() { Text = "Text1", Value = "Value1" },
        new() { Text = "Text2", Value = "Value2" },
        new() { Text = "Text3", Value = "Value3" },
        new() { Text = "Text4", Value = "Value4" },
    };

    // If your "ValueMember" is not a string, add its type as well
    // (eg. int, some custom enum, etc.)
    public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) => destinationType == typeof(string);
    public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) => sourceType == typeof(string);

    // Destination type is always string in a PropertyGrid but if your "ValueMember"
    // is some different type you might want to add it, too
    public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
    {
        if (destinationType != typeof(string) || value is not Item item)
            return base.ConvertTo(context, culture, value, destinationType);

        return item.Text;
    }

    // You might want to parse from string and the type of your "ValueMember".
    // Both are strings in your example.
    public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
    {
        if (value is not string str)
            return base.ConvertFrom(context, culture, value);

        // 1. Parsing by text, case-insensitive
        Item? result = _items.FirstOrDefault(i => String.Equals(i.Text, str, StringComparison.OrdinalIgnoreCase));

        // 2. Parsing by value, case-sensitive
        result ??= _items.FirstOrDefault(i => i.Value == str);

        return result ?? throw new ArgumentException($"Invalid value: {str}", nameof(value));
    }

    // This enables "ComboBox" for the property
    public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;

    // This tells that it's not a simple read-only drop down
    // but you can also type values to parse
    public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => false;

    // This returns the items to display in the drop-down area
    public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext? context) => new(_items);
}
3. Usage:

Place a PropertyGrid into a Form and use it like this:

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

        var myObj = new MyClassToEditInAGrid();
        propertyGrid1.SelectedObject = myObj;
    }
}

The result, demonstrating item selection and parsing from typed text or value:

Custom TypeConverter

Upvotes: 1

Related Questions