Sometimes you want to make a property read-only in PropertyGrid
based on some criteria at run-time. For example, let’s suppose you have a SampleClass
containing two properties, Editable
and StringProperty
and you want to make StringProperty
read-only if Editable
is false
.
We know that by adding a [ReadOnly(true)]
attribute to a property we can make it read-only. But how to do it at run-time based on some criteria?
In fact we can not add Attribute
to properties at run-time, but since PropertyGrid
uses Type Descriptor we can provide some solutions using type descriptor mechanism.
PropertyGrid
control uses the type descriptor of the object to extract information about its properties to show. The type descriptor, returns a list of PropertyDescriptor
objects as list of properties. Each PropertyDescriptor
contains some methods and properties to return display name, description, and other information about the property. IsReadOnly
property of the PropertyDescriptor
is responsible to inform the PropertyGrid
if the property should be read only.
Regarding to above fact, here are solutions which we can provide to make a property read-only at run-time in PropertyGrid
:
- Solution 1 – Provide a new
TypeDescriptor
for the class which returns aPropertyDescriptor
havingReadOnly
attribute for that specific property. -
Solution 2 – Add
ReadOnly
attribute toAttributeCollection
of theProperetyDescriptor
by handlingPropertyChanged
andSelectedObjcetsChanged
event ofPropertyGrid
. -
Solution 3 – Add
ReadOnly
attribute toAttributeCollection
of theProperetyDescriptor
in the class implementation based on some criteria.
Example
Let’s suppose we have a class containing two properties. Editable
and StringProperty
. If the Editable
is true
then StringProperty
should be editable, otherwise it would be read-only and will be shown as gray in PropertyGrid
.
This is the main goal: Making the property read-only in PropertyGrid
.
Here is the class:
using System;
public class SampleClass
{
public bool Editable { get; set; }
string stringProperty;
public string StringProperty
{
get { return stringProperty; }
set { if (Editable) stringProperty = value; }
}
}
Solution 1 – Providing a new TypeDescriptor
for the class
We can create a new TypeDescriptor
for the class. The new type descriptor, is responsible to provide a list of PropertyDescriptor
objects when PropertyGrid
asks about list of properties. The new type descriptor, should return a suitable PropertyDescriptor
for the property which we are going to make it readonly, to contain Read-Only
attribute based on some criteria.
To support extensibility, the
TypeDescriptor
class has a companion class calledTypeDescriptionProvider
and a companion attribute calledTypeDescriptionProviderAttribute
. You can use aTypeDescriptionProviderAttribute
on a class to introduce a completely different way of exposing metadata that meets your design goals.
Here is the implementation which is required for us to reach to the goal.
MyPropertyDescriptor
It’s responsible to provide metadata for property. When implementing this class, for most properties, we will use the trivial implementation which uses original property’s implementations, but for IsReadOnly
we will decide based on the value of Editable
property of the owner object:
using System;
using System.ComponentModel;
using System.Linq;
public class MyPropertyDescriptor : PropertyDescriptor
{
PropertyDescriptor p;
SampleClass o;
public MyPropertyDescriptor(PropertyDescriptor originalProperty, SampleClass owenr)
: base(originalProperty) { p = originalProperty; o = owenr; }
public override bool CanResetValue(object component)
{ return p.CanResetValue(component); }
public override object GetValue(object component) { return p.GetValue(component); }
public override void ResetValue(object component) { p.ResetValue(component); }
public override void SetValue(object component, object value)
{ p.SetValue(component, value); }
public override bool ShouldSerializeValue(object component)
{ return p.ShouldSerializeValue(component); }
public override AttributeCollection Attributes { get { return p.Attributes; } }
public override Type ComponentType { get { return p.ComponentType; } }
public override bool IsReadOnly { get { return !o.Editable; } }
public override Type PropertyType { get { return p.PropertyType; } }
}
MyTypeDescriptor
It’s responsible to provide a list of properties for the object. For StringProperty
which we are going to change its behavior at run-time, we will return a MyPropertyDescriptor
:
using System;
using System.ComponentModel;
using System.Linq;
public class MyTypeDescriptor : CustomTypeDescriptor
{
ICustomTypeDescriptor d;
SampleClass o;
public MyTypeDescriptor(ICustomTypeDescriptor originalDescriptor, SampleClass owner)
: base(originalDescriptor) { d = originalDescriptor; o = owner; }
public override PropertyDescriptorCollection GetProperties()
{ return this.GetProperties(new Attribute[] { }); }
public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
{
var properties = base.GetProperties(attributes).Cast<PropertyDescriptor>()
.Select(p => p.Name == "StringProperty" ? new MyPropertyDescriptor(p, o) : p)
.ToArray();
return new PropertyDescriptorCollection(properties);
}
}
MyTypeDescriptionProvider
It’s responsible to return a type descriptor for your object, when someone (like property grid) request type description:
using System;
using System.ComponentModel;
public class MyTypeDescriptionProvider : TypeDescriptionProvider
{
public MyTypeDescriptionProvider()
: base(TypeDescriptor.GetProvider(typeof(object))) { }
public override ICustomTypeDescriptor GetTypeDescriptor(Type type, object o)
{
ICustomTypeDescriptor baseDescriptor = base.GetTypeDescriptor(type, o);
return new MyTypeDescriptor(baseDescriptor, (SampleClass)o);
}
}
SampleClass
At last, the implementation of the class should be changed:
using System;
using System.ComponentModel;
[TypeDescriptionProvider(typeof(MyTypeDescriptionProvider))]
public class SampleClass
{
[RefreshProperties(RefreshProperties.All)]
public bool Editable { get; set; }
string sp;
public string StringProperty
{
get { return sp; }
set
{
if (Editable)
sp = value;
}
}
}
[RefreshProperties(RefreshProperties.All)]
informs the PropertyGrid
that should refresh all properties appearance when Editable
property changed.
Solution 2 – Handling PropertyGrid
events and adding ReadOnlyAttribute
Regarding to the example, we want to have the StringProperty
of our class be read-only in PropertyGrid
. As an option, we can handle PropertyChanged
and SelectedObjectsChanged
event of the class and add ReadOnly
attribute to AttributeCollection
which is returned by Attributes
property of PropertyDescriptor
of the StringProperty
. To do so, we need to use reflection to add an attribute to the AttributeCollection
.
PropertyDescriptorExtensions
Here I created an extension method which allows us to add ReadOnly
attribute to a PropertyDescriptor:
using System;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
public static class PropertyDescriptorExtensions
{
public static void SetReadOnlyAttribute(this PropertyDescriptor p, bool value)
{
var attributes = p.Attributes.Cast<Attribute>()
.Where(x => !(x is ReadOnlyAttribute)).ToList();
attributes.Add(new ReadOnlyAttribute(value));
typeof(MemberDescriptor).GetProperty("AttributeArray",
BindingFlags.Instance | BindingFlags.NonPublic)
.SetValue((MemberDescriptor)p, attributes.ToArray());
}
}
PropertyChanged and SelectedObjectsChanged
Handling PropertyChanged
event is because we want to change the appearance of the property in PropertyGrid
based on change in Editable
property value. Also handling SelectedObjectsChanged
is because we want to make sure that after we assigned an instance of the SampleClass
to propertyGrid1.SelectedObject
the StringProperty
is read-only if required:
private void propertyGrid1_PropertyValueChanged(object s, PropertyValueChangedEventArgs e)
{
var obj = this.propertyGrid1.SelectedObject as SampleClass;
if(obj!=null)
{
TypeDescriptor.GetProperties(this.propertyGrid1.SelectedObject)
["StringProperty"].SetReadOnlyAttribute(!obj.Editable);
propertyGrid1.Refresh();
}
}
private void propertyGrid1_SelectedObjectsChanged(object sender, EventArgs e)
{
var obj = this.propertyGrid1.SelectedObject as SampleClass;
if (obj != null)
{
TypeDescriptor.GetProperties(this.propertyGrid1.SelectedObject)
["StringProperty"].SetReadOnlyAttribute(!obj.Editable);
propertyGrid1.Refresh();
}
}
SampleClass
In this solution the SampleClass
implementation will remain untouched:
using System;
public class SampleClass
{
public bool Editable { get; set; }
string stringProperty;
public string StringProperty
{
get { return stringProperty; }
set { if (Editable) stringProperty = value; }
}
}
Solution 3 – Add ReadOnly
attribute on the fly in the class implementation based on some criteria
SampleClass
Implementation of the sample class should change:
public class SampleClass:Component
{
bool editable;
[RefreshProperties(RefreshProperties.All)]
public bool Editable
{
get { return editable; }
set
{
editable = value;
TypeDescriptor.GetProperties(this)["StringProperty"].SetReadOnlyAttribute(!editable);
}
}
string stringProperty;
[ReadOnly(true)]
public string StringProperty
{
get { return stringProperty; }
set
{
if (Editable)
stringProperty = value;
}
}
}
The initial [ReadOnly(true)]
for the property is because we want it to be read-only by default and change the it at run-time based on the change in Editable
property value.
Conclusion
The main difference between above 3 solutions is in changing or not changing the the existing code and also in designer support:
-
TypeDescrpitor
Solution: The solution is attribute based and without changing the logic of the class, you can provide custom metadata for the class at run-time based on some criteria. The changes in the class code, is just because of adding some attributes and the logic of the class will not change. This solution has designer support. -
PropertyGrid Events: The solution can satisfy the requirement without changing the existing code. The changes are just in UI layer. This solution doesn’t have designer support and is based on
PropertyGrid
control events at run-time. -
Adding
RaedOnly
attribute and changing the value at run-time: The solution works well, but you need to change the logic of the code to changeReadOnly
attribute on the fly. This solution has designer support.
Notes
-
The post is written to extend and support the answer which I posted for this question in stackoverflow: .Net how to set IsReadOnly for a property for a UserControl
-
In the first version of the post, for
SetReadOnlyAttribute
I was changingisReadOnly
field ofReadOnly
attribute. An important risk catched by Ivan Stoev. Using that solution, there is a risk to change the value of static fieldsReadOnlyAttribute.Yes
,No
orDefault
, because some property descriptors use those static values. I fixed the problem by removing the existingReadOnly
attribute and adding a new attribute instead.
Awesome awesome. thank you please please can you make a full tutorials for WinForms Designer and Controls Design.
Examples:
1.when to use NotifyPropertyChanged(), RefreshProperties(All,Repaint)
2.How to make a list of dynamic objects which came from SQL (DataSource, DataMember) properties of controls.
etc
Thank you. please keep going… 🙂
While this does work, it appears to be quite brittle; any changes to the Sample class results in an an InvalidCastException in the designer.