Saturday, October 4, 2008

Changing Attribute Parameters at Runtime

A question was asked on the C# forums yesterday regarding how to set a field in a PropertyGrid to readonly during runtime. Just in case you're not familiar with the PropertyGrid in C#, it's the control you use to set items in the "Properties" window in Visual Studio. Unfortunately, the .NET implementation of this control for use in Windows Forms is pretty basic and doesn't provide much functionality in the way of setting certain fields to readonly, etc. Instead, it depends on the attributes applied to the object it's editing to determine it's behavior. For instance, to make a property reaonly within the PropertyGrid, the ReadOnlyAttribute needs to be applied to that property within the object the PropertyGrid is editing. This makes things a little more difficult from a runtime perspective, because attribute values cannot easily be set at runtime. Nevertheless, what follows are detailed instructions on how to do just that, and have the PropertyGrid update itself accordingly.

Follow along with my example:

1. Create a new Windows Forms project.

2. Add the following class to your project:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Reflection;
 
namespace WindowsFormsApplication1
{
public class Employee
{
string name;
bool isMarried;
string spouseName;
 
public string Name
{
get { return name; }
set { name = value; }
}
 
public bool IsMarried
{
get { return isMarried; }
set
{
isMarried = value;
 
// set the spouseName to empty if the employee is not married.
if (!value)
spouseName = string.Empty;
}
}
 
public string SpouseName
{
get { return spouseName; }
set { spouseName = value; }
}
}
}
3. Add a PropertyGrid to your form, leaving the default name of "propertyGrid1".

4. Add the following line to the form's constructor, just after the call to InitializeComponent():


propertyGrid1.SelectedObject = new Employee();
5. Run the project.

You should see a screen like this:



Add some text in the SpouseName field, and then double click on the IsMarried value a couple of times. Notice that when the IsMarried property is set to true, the SpouseName field is set to an empty string. This is because we're changing the value within our class to an empty string if the IsMarried property is false. We really don't want a spouse for the employee, if the employee isn't married. Here's the problem however: you could set the IsMarried field to false, and still have a SpouseName value, simply by setting the SpouseName field after setting the IsMarried value to false. Of course, this isn't what we want. It would be far more user friendly to simply disable that entire field, and prevent the user entirely from changing the SpouseName in the PropertyGrid.

Let's add the ReadOnlyAttribute to the SpouseName property in the object:


[ReadOnly(true)]
public string SpouseName
{
get { return spouseName; }
set { spouseName = value; }
}
Now, rerun the application. The form now looks like this:



Notice the SpouseName is readonly, but it's permanently readonly. That's not what we want either. We only want SpouseName to be readonly when IsMarried is set to false. So here's the solution. We need to manually change the ReadOnly value of this specific ReadOnlyAttribute at runtime. I grabbed a copy of Lutz's .NET Reflector and browsed the internal workings of the ReadOnlyAttribute class. I found that there's actually an internal field called "isReadOnly" that determines whether the ReadOnly property of the ReadOnlyAttribute is true or false. Using TypeDescriptor, PropertyDescriptor and Reflection, I can set this value at runtime, and then I can notify the PropertyGrid to refresh all the data.

Change your IsMarried property to this:


[RefreshProperties(RefreshProperties.All)]
public bool IsMarried
{
get { return isMarried; }
set
{
isMarried = value;
 
// set the spouseName to empty if the employee is not married.
if (!value)
spouseName = string.Empty;
 
// Create a PropertyDescriptor for "SpouseName" by calling the static GetProperties on TypeDescriptor.
PropertyDescriptor descriptor = TypeDescriptor.GetProperties(this.GetType())["SpouseName"];
 
// Fetch the ReadOnlyAttribute from the descriptor.
ReadOnlyAttribute attrib = (ReadOnlyAttribute)descriptor.Attributes[typeof(ReadOnlyAttribute)];
 
// Get the internal isReadOnly field from the ReadOnlyAttribute using reflection.
FieldInfo isReadOnly = attrib.GetType().GetField("isReadOnly", BindingFlags.NonPublic | BindingFlags.Instance);
 
// Using Reflection, set the internal isReadOnly field.
isReadOnly.SetValue(attrib, !value);
 
}
}
Now run your application again. You should notice now, that when you set the IsMarried property to false, the SpouseName becomes readonly, but when the IsMarried property is set to true, you can edit the field. There's two reasons for this:

1. We've added the RefreshPropertiesAttribute to the property. This ensures that the PropertyGrid is refreshed whenever that particular property is changed, forcing the PropertyGrid to reload all the property attributes on the object.

2. We've manually changed the ReadOnly property on the ReadOnly attribute of the SpouseName property to false within the setter for IsMarried. The PropertyGrid will reload this attribute now when refreshing itself.

There are several applications for this, the biggest of which being conditional readonly properties on certain user controls that can be designed within Visual Studio. Other applications could be for applications that want to use the built-in functionality of PropertyGrid to edit business objects at runtime.