Search code examples
c#.netwinformspropertygrid

How to use PropertyGrid to allow editing properties without a setter?


By default, PropertyGrid only allowed editing properties with public setter. I'd like to allow editing of properties without a setter.

For example:

class A {

   public int X {get;set}

   public int Y {get;}
}

In the example above, only X will be editable. Y will be displayed but grayed out. How can I make Y editable?

Note: making a private backing field would be OK. For example:

class A {

   public int X {get;set}

   private int y;
   public int Y {get => y; }
}

Solution

  • You can build a wrapper/proxy class based on the ICustomTypeDescriptor Interface that allows you to tweak properties at runtime.

    This is how you could use it:

    var a = new A();
    
    // build a proxy
    var proxy = new Proxy(a);
    
    // tweak any properties
    proxy.Properties["Y"].IsReadOnly = false;
    // you can also tweak attributes
    proxy.Properties["Y"].Attributes.Add(new CategoryAttribute("R/O -> R/W"));
    proxy.Properties["Y"].Attributes.Add(new DescriptionAttribute("This works"));
    
    // handle property change
    propertyGrid1.PropertyValueChanged += (s, e) =>
    {
        if (e.ChangedItem.PropertyDescriptor.Name == "Y")
        {
            a.Y = (int)e.ChangedItem.Value;
        }
    };
    
    // select the proxy instead of the original instance
    propertyGrid1.SelectedObject = proxy;
    

    And here is the result

    enter image description here

    ...
    class A
    {
        public int X { get; set; }
        public int Y { get; internal set; }
    }
    
    ...
    
    
    public class Proxy : ICustomTypeDescriptor
    {
        public Proxy(object instance)
        {
            if (instance == null)
                throw new ArgumentNullException(nameof(instance));
    
            Instance = instance;
            Properties = TypeDescriptor.GetProperties(instance).OfType<PropertyDescriptor>().Select(d => new ProxyProperty(instance, d)).ToDictionary(p => p.Name);
        }
    
        public object Instance { get; }
        public IDictionary<string, ProxyProperty> Properties { get; }
    
        public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(Instance);
        public string GetClassName() => TypeDescriptor.GetClassName(Instance);
        public string GetComponentName() => TypeDescriptor.GetComponentName(Instance);
        public TypeConverter GetConverter() => TypeDescriptor.GetConverter(Instance);
        public EventDescriptor GetDefaultEvent() => TypeDescriptor.GetDefaultEvent(Instance);
        public object GetEditor(Type editorBaseType) => TypeDescriptor.GetEditor(Instance, editorBaseType);
        public EventDescriptorCollection GetEvents() => TypeDescriptor.GetEvents(Instance);
        public EventDescriptorCollection GetEvents(Attribute[] attributes) => TypeDescriptor.GetEvents(Instance, attributes);
        public PropertyDescriptor GetDefaultProperty() => TypeDescriptor.GetDefaultProperty(Instance);
        public PropertyDescriptorCollection GetProperties() => new PropertyDescriptorCollection(Properties.Values.Select(p => new Desc(this, p)).ToArray());
        public PropertyDescriptorCollection GetProperties(Attribute[] attributes) => GetProperties();
        public object GetPropertyOwner(PropertyDescriptor pd) => Instance;
    
        private class Desc : PropertyDescriptor
        {
            public Desc(Proxy proxy, ProxyProperty property)
                : base(property.Name, property.Attributes.ToArray())
            {
                Proxy = proxy;
                Property = property;
            }
    
            public Proxy Proxy { get; }
            public ProxyProperty Property { get; }
    
            public override Type ComponentType => Proxy.GetType();
            public override Type PropertyType => Property.PropertyType ?? typeof(object);
            public override bool IsReadOnly => Property.IsReadOnly;
            public override bool CanResetValue(object component) => Property.HasDefaultValue;
            public override object GetValue(object component) => Property.Value;
            public override void ResetValue(object component) { if (Property.HasDefaultValue) Property.Value = Property.DefaultValue; }
            public override void SetValue(object component, object value) => Property.Value = value;
            public override bool ShouldSerializeValue(object component) => Property.ShouldSerializeValue;
        }
    }
    
    public class ProxyProperty
    {
        public ProxyProperty(string name, object value)
        {
            if (name == null)
                throw new ArgumentNullException(nameof(value));
    
            Name = name;
            Value = value;
            Attributes = new List<Attribute>();
        }
    
        public ProxyProperty(object instance, PropertyDescriptor descriptor)
        {
            if (descriptor == null)
                throw new ArgumentNullException(nameof(descriptor));
    
            Name = descriptor.Name;
            Value = descriptor.GetValue(instance);
            var def = descriptor.Attributes.OfType<DefaultValueAttribute>().FirstOrDefault();
            if (def != null)
            {
                HasDefaultValue = true;
                DefaultValue = def.Value;
            }
    
            IsReadOnly = (descriptor.Attributes.OfType<ReadOnlyAttribute>().FirstOrDefault()?.IsReadOnly).GetValueOrDefault();
            ShouldSerializeValue = descriptor.ShouldSerializeValue(instance);
            Attributes = descriptor.Attributes.Cast<Attribute>().ToList();
            PropertyType = descriptor.PropertyType;
        }
    
        public string Name { get; }
        public object Value { get; set; }
        public object DefaultValue { get; set; }
        public bool HasDefaultValue { get; set; }
        public bool IsReadOnly { get; set; }
        public bool ShouldSerializeValue { get; set; }
        public Type PropertyType { get; set; }
        public IList<Attribute> Attributes { get; }
    }