See the question and my original answer on StackOverflow

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