propertygridtypeconvertersystem.componentmodel

How to get the value of a runtime property back into a design time property?


I have something like

public class A
{
  public string FullName { get; set; }
}

A class that I don't want to change.

In a PropertyGrid I want it too look like this:

In the PropertyGrid it would look like this:

FullName   | John Doe
  ForeName | John
  LastName | Doe

So I want to compose A.FullName from two properties ( probably created at runtime )

My idea was to use

public class FullName
{
  public string Forename { get; set; }
  public string LastName { get; set; }
}

to replace the actual A.FullName property with that composite property in A's custom TypeDescriptor

I know how to implement the ExpandableObjectConverter, but I am not sure how to get that composed name back into the original string property.

Usually I use TypeDescriptor.CreateProperty to replace properties with modified versions ( added attributes for example ). But there I don't have to worry about getting the value into the original property as its PropertyDescriptor is one of that methods arguments.


Solution

  • Assuming you know how to dynamically add a TypeConverter to a property, as seen in How to get a runtime list into a PropertyGrid?, you need to build a a converter with properties that remember the initial instance that's being worked on.

    Here is some sample code you can test like this:

    public class A
    {
        [TypeConverter(typeof(FullNameConverter))] // for quick demo purpose
        public string FullName { get; set; } = "John Doe";
    }
    
    // a class that knows how to parse and format a full name
    public class FullName
    {
        public string Forename { get; set; }
        public string LastName { get; set; }
    
        public override string ToString() => $"{Forename} {LastName}";
    
        public static FullName Parse(string? fullName)
        {
            if (string.IsNullOrWhiteSpace(fullName))
                return new FullName();
    
            var split = fullName.Split(' ');
            return new FullName { Forename = split[0], LastName = split[1] };
        }
    }
    
    // A <=> FullName property descriptors (they remember the A instance)
    public class FullNamePropertyDescriptor : PropertyDescriptor
    {
        // we use initial value to be able to reset (feature sometimes used by property grids)
        public FullNamePropertyDescriptor(A instance, object initialValue, string name, Type propertyType, Attribute[]? attrs)
            : base(name, attrs)
        {
            Instance = instance;
            PropertyType = propertyType;
            InitialValue = GetValue(initialValue);
        }
    
        public A Instance { get; }
        public object? InitialValue { get; }
    
        public override Type ComponentType => Instance.GetType();
        // honor ReadOnly attributes on FullName properties
        public override bool IsReadOnly => Attributes.OfType<ReadOnlyAttribute>().FirstOrDefault()?.IsReadOnly ?? false;
        public override Type PropertyType { get; }
        public override bool CanResetValue(object component) => true;
        public override bool ShouldSerializeValue(object component) => false;
        public override void ResetValue(object component) => SetValue(component, InitialValue);
    
        // component is a string here
        public override object? GetValue(object? component)
        {
            var fn = FullName.Parse(component as string);
    
            // defer to FullName properties
            return TypeDescriptor.GetProperties(typeof(FullName))[Name].GetValue(fn);
        }
    
        // component is a string here
        public override void SetValue(object? component, object? value)
        {
            var fn = FullName.Parse(component as string);
    
            // defer to FullName properties
            TypeDescriptor.GetProperties(typeof(FullName))[Name].SetValue(fn, value);
    
            // update original object
            Instance.FullName = fn.ToString();
        }
    }
    
    // the A <=> FullName type converter
    public class FullNameConverter : TypeConverter
    {
        public override bool GetPropertiesSupported(ITypeDescriptorContext? context) => true;
        public override PropertyDescriptorCollection? GetProperties(ITypeDescriptorContext? context, object value, Attribute[]? attributes)
            => new([.. TypeDescriptor.GetProperties(typeof(FullName))
                .OfType<PropertyDescriptor>()
                .Select(pd => new FullNamePropertyDescriptor(
                    (A)context.Instance,
                    value,
                    pd.Name,
                    pd.PropertyType,
                    pd.Attributes.Cast<Attribute>().ToArray()))]
                );
    
        // we can convert to and from string (the underlying type)
        public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType) => typeof(string) == destinationType || base.CanConvertTo(context, destinationType);
        public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) => typeof(string) == sourceType || base.CanConvertFrom(context, sourceType);
        public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) => value;
        public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) => value;
    }
    

    Here's the result:

    enter image description here