2

I am trying to implement a drop-down property in a custom component, and I used This SO Answer and this answer as a guide.

So far I managed to get it working, with predefined items in the drop-down list.
But I still need to figure out how to alter the items in the drop-down list ?

This is the code I have so far (build from the link mentioned above)

[TypeConverter(typeof(TableNameConverter))]
public TableName gttTableName
{ get; set; }

...

public class TableName
{
    public string Name { get; set; }

    public override string ToString()
    {
        return $"{Name}";
    }
}

public class TableNameService
{
    List<TableName> list = new List<TableName>() {
        new TableName() {Name = "naam 1" },
        new TableName() {Name = "naam 2" },
    };

    public IEnumerable<TableName> GetAll()
    {
        return list;
    }
}

public class TableNameConverter : TypeConverter
{
    public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
    {
        var svc = new TableNameService();
        return new StandardValuesCollection(svc.GetAll().ToList());
    }

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

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

    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
        if (sourceType == typeof(string))
            return true;
        return base.CanConvertFrom(context, sourceType);
    }

    public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
    {
        if (value != null && value.GetType() == typeof(string))
        {
            var v = $"{value}";
            //var id = int.Parse(v.Split('-')[0].Trim());
            var name = v.ToString();
            var svc = new TableNameService();
            //return svc.GetAll().Where(x => x.Id == id).FirstOrDefault();
            return svc.GetAll().Where(x => x.Name == name).FirstOrDefault();
        }
        return base.ConvertFrom(context, culture, value);
    }
}

This looks like this in VS property window

enter image description here

Now for the problem, when a certain property changes then the items in the drop-down property must change. This should be done in the method UpdateTableNames(code is below).
In other words, in the setter of another property the items naam 1, naam 2 can change to a complete new set, with more or less items and with different values.
What I can not figure out yet is how can I alter these items ?

Let me explain the situation.

  • A user drops the custom component on a form
  • when he looks at the property gttTableName it will now show naam 1 and naam 2
  • Now the user changes another property (gttDataModule) and in the setter of this property the items of the gttTableName property can change.
  • So if he looks again at the property gttTableName it should now show a complete other list of values.

The code for the property gttDataModule is this

public gttDataModule gttDataModule
{
    get { return _gttDataModule; }
    set
    {
        _gttDataModule = value;
        UpdateTableNames();
    }
}

private void UpdateTableNames()
{
    List<string> tableNames = new List<string>();
    if (_gttDataModule != null)
    {
        foreach (gttDataTable table in _gttDataModule.gttDataTables)
        {
            tableNames.Add(table.Table.TableName);
        }
    }

    // at this point the list tableNames will be populated with values.
    // What I need is to replace the list in TableNameService from 'naam 1', 'naam 2' 
    // to the values in this list.
    // so they won't be 'naam 1' and 'naam 2' anymore 
    // It could be more or less items or even none 
    // They could have different values
    // for example the list could be 'tblBox', 'tblUser', tblClient', tblOrders'
    // or it could be empty
    // or it could be 'vwCars', 'tblSettings'
}

How can I change the items in the list of the TableNameService ?

Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
GuidoG
  • 11,359
  • 6
  • 44
  • 79

2 Answers2

1

I managed to do it, but am unsure if it's a good and stable solution.

What I did is create a static class to hold the list

public static class NameList
{
    public static List<TableName> list = new List<TableName>();

    public static void UpdateItems(List<string> tableNames)
    {
        list.Clear();
        foreach (string item in tableNames)
        {
            list.Add(new TableName() { Name = item });
        }
    }

}

Then used that list in class TableNameService

public class TableNameService
{
    List<TableName> list = NameList.list;

    public IEnumerable<TableName> GetAll()
    {
        return list;
    }
}

and now I can update the list from the setter from another property like this

    private void UpdateTableNames()
    {
        List<string> tableNames = new List<string>();
        if (_gttDataModule != null)
        {
            foreach (gttDataTable table in _gttDataModule.gttDataTables)
            {
                tableNames.Add(table.Table.TableName);
            }
        }

        NameList.UpdateItems(tableNames);
    }

I tried it and it actual works, the list is changing correct.

But I would like some comments on this solution, are there any problems I have not foreseen. What are the drawbacks on doing it this way ?

GuidoG
  • 11,359
  • 6
  • 44
  • 79
  • You have got the idea and this is the good part. But I don't have a good feeling about those static classes/methods. For example have you tried to see what happens if you have two PropertyGrid in the same page in the same form and try editing those property grids? – Reza Aghaei Jan 22 '21 at 12:11
  • @RezaAghaei I feel the same, something does not feel right. But I have not found another way yet to do it. There must be a proper and elegant way to make a property like this. Maybe I need to figure out how to use the editor like DataBinding/Text property of an TextBox for example – GuidoG Jan 22 '21 at 12:19
1

I would create an example based on the answer in this post: PropertyGrid - Load dropdown values dynamically.

The basic idea is having custom type converter and overriding GetStandardValues to return a list of supported values for the editing property.

The point here is using the context.Instance to get an instance of the object which is editing and trying to filter the list based on the other properties of the editing object.

In the following example, I'll edit a Product which has a Category and a SubCategory property and there's a relation between categories and sub-categories. For example if you choose category 1, then the sub-categories list should show the sub-categories of category 1, but if you choose category 2, then the list should show a different group of sub categories, like this:

enter image description here

And this is the code for the example:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    private Category category;
    [TypeConverter(typeof(CategoryConverter))]
    public Category Category
    {
        get { return category; }
        set
        {
            if (category?.Id != value?.Id)
            {
                category = value;
                SubCategory = null;
            }
        }
    }
    [TypeConverter(typeof(SubCategoryConverter))]
    public SubCategory SubCategory { get; set; }
}
public class Category
{
    public int Id { get; set; }
    public string Name { get; set; }
    public override string ToString()
    {
        return $"{Id} - {Name}";
    }
}
public class SubCategory
{
    public int Id { get; set; }
    public int CategoryId { get; set; }
    public string Name { get; set; }
    public override string ToString()
    {
        return $"{Id} - {Name}";
    }
}
public class CategoryService
{
    List<Category> categories = new List<Category> {
        new Category() { Id = 1, Name = "Category 1" },
        new Category() { Id = 2, Name = "Category 2" },
    };
    List<SubCategory> subCategories = new List<SubCategory> {
        new SubCategory() { Id = 11, Name = "Sub Category 1-1", CategoryId= 1 },
        new SubCategory() { Id = 12, Name = "Sub Category 1-2", CategoryId= 1 },
        new SubCategory() { Id = 13, Name = "Sub Category 1-3", CategoryId= 1 },
        new SubCategory() { Id = 21, Name = "Sub Category 2-1", CategoryId= 2 },
        new SubCategory() { Id = 22, Name = "Sub Category 2-2", CategoryId= 2 },
    };
    public IEnumerable<Category> GetCategories()
    {
        return categories;
    }
    public IEnumerable<SubCategory> GetSubCategories()
    {
        return subCategories.ToList();
    }
}
public class CategoryConverter : TypeConverter
{
    public override StandardValuesCollection GetStandardValues(
        ITypeDescriptorContext context)
    {
        var svc = new CategoryService();
        return new StandardValuesCollection(svc.GetCategories().ToList());
    }
    public override bool GetStandardValuesSupported(
        ITypeDescriptorContext context)
    {
        return true;
    }
    public override bool GetStandardValuesExclusive(
        ITypeDescriptorContext context)
    {
        return true;
    }
    public override bool CanConvertFrom(ITypeDescriptorContext context,
        Type sourceType)
    {
        if (sourceType == typeof(string))
            return true;
        return base.CanConvertFrom(context, sourceType);
    }
    public override object ConvertFrom(ITypeDescriptorContext context,
        CultureInfo culture, object value)
    {
        if (value != null && value.GetType() == typeof(string))
        {
            var v = $"{value}";
            var id = int.Parse(v.Split('-')[0].Trim());
            var svc = new CategoryService();
            return svc.GetCategories()
                .Where(x => x.Id == id).FirstOrDefault();
        }
        return base.ConvertFrom(context, culture, value);
    }
}
public class SubCategoryConverter : TypeConverter
{
    public override StandardValuesCollection GetStandardValues(
        ITypeDescriptorContext context)
    {
        var svc = new CategoryService();
        var categoryId = ((Product)context.Instance).Category.Id;
        return new StandardValuesCollection(svc.GetSubCategories()
            .Where(x => x.CategoryId == categoryId).ToList());
    }
    public override bool GetStandardValuesSupported(
        ITypeDescriptorContext context)
    {
        return true;
    }
    public override bool GetStandardValuesExclusive(
        ITypeDescriptorContext context)
    {
        return true;
    }
    public override bool CanConvertFrom(ITypeDescriptorContext context, 
        Type sourceType)
    {
        if (sourceType == typeof(string))
            return true;
        return base.CanConvertFrom(context, sourceType);
    }
    public override object ConvertFrom(ITypeDescriptorContext context, 
        CultureInfo culture, object value)
    {
        if (value != null && value.GetType() == typeof(string))
        {
            var v = $"{value}";
            var id = int.Parse(v.Split('-')[0].Trim());
            var svc = new CategoryService();
            return svc.GetSubCategories()
                .Where(x => x.Id == id).FirstOrDefault();
        }
        return base.ConvertFrom(context, culture, value);
    }
}
Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
  • Having 2 type converter is not necessary, it's just for the example purpose. The main point here is using the `context.Instance` to get an instance of the object which is editing and trying to filter the list based on the other properties of the editing object. And for the classes which provide the list, I look into them like repositories, they contain everything, you load/filter what you need. – Reza Aghaei Jan 22 '21 at 12:30
  • Anyways, you know more about your requirements which may be different from what I tried to show here, but I'm sure you have got the idea. Hope it helps :) – Reza Aghaei Jan 22 '21 at 12:31
  • Another point is about changing the items in the repository: I assume the repository is loading data from a permanent storage and updating data of the permanent storage is not a concern here. However nothing stops you from having methods to update an in memory repository. In this case (in-memory repository, having the memory storage as a static list completely makes sense, not smelly at all.) – Reza Aghaei Jan 22 '21 at 12:34
  • Thank you for the example, it is very educational. However in my case the items in the subcategory are not fixed. They are provided by another component and could be literal anything. So i guess I go for the static list option which I can manipulate. – GuidoG Jan 22 '21 at 13:49
  • Reza, In my question I wanted to link to the same link you gave here, your answer in another question. I noticed that somehow I managed to put the wrong link there, alltough this link also covers the same subject but not quite as good. I edited my question so this error is now corrected – GuidoG Jan 22 '21 at 13:53
  • No problem, You definitely know your requirements better :) – Reza Aghaei Jan 22 '21 at 14:02