Search code examples
c#.netvb.netwinformspropertygrid

PropertyGrid - Load dropdown values dynamically


I've tried all sorts - lots of messing around with TypeConverters etc. So I won't cover all that here.

So to reduce the question to its basics. Considering the below:

Imports LiteDB
Imports System.ComponentModel

Public Class mSystem

    <CategoryAttribute("General"), ReadOnlyAttribute(True)>
    Public Property ID As Integer

    Public Property ShortName As String = ""

    <BsonRef("mSystemTypes")>
    Public Property SystemType As mSystemType

End Class

Public Class mSystemType

    <CategoryAttribute("General"), ReadOnlyAttribute(True)>
    Public Property ID As Integer

    Public Property Name As String = "Default"
    Public Property Details As String = ""

End Class

How do I get "SystemTypes" as a dropdown selector dynamically filled from a mSystemTypes collection? E.g. You select "Console" and it updates mSystem with the matching mSystemType.

I am using LiteDb, which may make things a little more difficult, as it isn't purely an integer for the 'foreign key' as other scenarios may present, but a full object.

I need to maintain the 'dbRef' approach to ensure integrity of data relationships. Just in case the DBLite thing throws in a curve ball, some code below demonstrating its use:

Public Class dbCollecitons

    Public mSystems As LiteCollection(Of mSystem)
    Public mSystemTypes As LiteCollection(Of mSystemType)

    Private Sub Examples()

        Dim col As LiteCollection(Of mSystemType) = dbCollections.mSystemTypes
        Dim value as String = "Console"
        Dim q = col.FindAll.Where(Function(x) x.Name = value).First
        Console.Writeline(q.ID)

    End Sub

End Class

LiteDb.LiteCollection doesn't map directly onto ICollection (you use this in TypeConverter?), but I'm sure there's some work around.


Solution

  • In short, you need to create a new TypeConverter supporting standard values.

    Example - VB.NET

    I suppose you have a Product class, having a property of type Category which you want to be able to choose from a List<Category> which is coming from somewhere like database at run-time:

    Public Class Product
        Public Property Id As Integer
        Public Property Name As String
        <TypeConverter(GetType(CategoryConverter))>
        Public Property Category As Category
    End Class
    
    Public Class Category
        Public Property Id As Integer
        Public Property Name As String
        Public Overrides Function ToString() As String
            Return $"{Id} - {Name}"
        End Function
    End Class
    

    And here is the CategoryService class which can load categories from wherever you like:

    Public Class CategoryService
        Private list As List(Of Category) = New List(Of Category) From {
            New Category() With {.Id = 1, .Name = "Category 1"},
            New Category() With {.Id = 2, .Name = "Category 2"},
            New Category() With {.Id = 3, .Name = "Category 3"}
        }
        Public Function GetAll() As IEnumerable(Of Category)
            Return list
        End Function
    End Class
    

    The next step is creating CategoryConverter which is responsible to provide values for the dropdown:

    Imports System.ComponentModel
    Public Class CategoryConverter
        Inherits TypeConverter
        Public Overrides Function GetStandardValues(ByVal context As ITypeDescriptorContext) As StandardValuesCollection
            Dim svc = New CategoryService()
            Return New StandardValuesCollection(svc.GetAll().ToList())
        End Function
        Public Overrides Function GetStandardValuesSupported(ByVal context As ITypeDescriptorContext) As Boolean
            Return True
        End Function
        Public Overrides Function GetStandardValuesExclusive(ByVal context As ITypeDescriptorContext) As Boolean
            Return True
        End Function
        Public Overrides Function CanConvertFrom(ByVal context As ITypeDescriptorContext, ByVal sourceType As Type) As Boolean
            If sourceType = GetType(String) Then Return True
            Return MyBase.CanConvertFrom(context, sourceType)
        End Function
        Public Overrides Function ConvertFrom(context As ITypeDescriptorContext, culture As CultureInfo, value As Object) As Object
            If value IsNot Nothing AndAlso value.[GetType]() = GetType(String) Then
                Dim v = $"{value}"
                Dim id = Integer.Parse(v.Split("-"c)(0).Trim())
                Dim svc = New CategoryService()
                Return svc.GetAll().Where(Function(x) x.Id = id).FirstOrDefault()
            End If
            Return MyBase.ConvertFrom(context, culture, value)
        End Function
    End Class
    

    Then as a result, when you set an instance of Product as SelectedObject of the PropertyGrid, for Category property, you choose a value from list.

    Example - C#

    Here is the C# version of the above example:

    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        [TypeConverter(typeof(CategoryConverter))]
        public Category Category { get; set; }
    }
    public class Category
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public override string ToString()
        {
            return $"{Id} - {Name}";
        }
    }
    
    public class CategoryService
    {
        List<Category> list = new List<Category>{
            new Category() { Id = 1, Name = "Category 1" },
            new Category() { Id = 2, Name = "Category 2" },
            new Category() { Id = 3, Name = "Category 3" },
        };
        public IEnumerable<Category> GetAll()
        {
            return list;
        }
    }
    
    public class CategoryConverter : TypeConverter
    {
        public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
        {
            var svc = new CategoryService();
            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 svc = new CategoryService();
                return svc.GetAll().Where(x => x.Id == id).FirstOrDefault();
            }
            return base.ConvertFrom(context, culture, value);
        }
    }