Make Your Components Really RAD with Visual Studio .NET Property Browser
Shawn Burke
Microsoft Corporation
Updated February 2002
Summary: This article will help you explore the Microsoft Visual Studio .NET property browser and take advantage of its new features. (23 printed pages)
Download Codesamples.exe.
Contents
Introduction
What Can It Do?
The Basics: Using Attributes to Customize Browsing
Expandable Properties and String Conversion: TypeConverters and the Property Browser
Editing and Displaying Custom Types
Providing Alternate Property Views
And You Can Use It Too!
Conclusion
Introduction
Since the introduction of Microsoft® Visual Basic® in the early days of Windows®, the property browser has been a pivotal element in allowing truly Rapid Application Development (often referred to as RAD). In Microsoft Visual Studio® .NET, this tradition continues with the added benefit of many property browser features not previously available. If you are writing components or other objects that participate in the Visual Studio .NET environment, you will surely want to take advantage of many of those new features to really make your components great for your users.
What Can It Do?
Prior versions of the property browser essentially processed COM-type information and displayed the properties that were contained therein. A COM component's public API is usually defined in Interface Definition Language (IDL) and has a fixed set of attributes attached to it, such as nonbrowsable, which prevents property browsers from seeing it, or bindable, which marks a property as eligible for data-binding. Other browsing features, such as standard value lists or categorized properties, had required component authors to implement COM interfaces such as IPerPropertyBrowsing or ICategorizeProperties. The .NET Framework and the Visual Studio .NET property browser, which is written using .NET Framework Windows Forms classes, offers these features in a simpler, more unified way, and provides a host of new and exciting features.
Of course, the Visual Studio .NET property browser still supports the same features as prior versions —it knows how to inspect type information from ITypeInfo and how to support the extended browsing interfaces noted above. Nevertheless, if you want to access most of the compelling new features, you will need to write your components on top of the .NET Framework using managed code. Below is a brief list of these great new features and what they do:
- Metadata attributes
Attributes on properties determine a great deal about how the property browser behaves. They allow the component author to control with ease what properties are visible, how they are categorized, whether they can be included in a multi-selection, and whether they affect the values of other properties. These attributes are straightforward to learn and utilize. - Hierarchical support
Properties can be expanded into sub-properties for logical organization. - Graphical value representation
Along with the text for a property's value, a small graphical representation may be drawn, such as a sample of a chosen color or font. - Custom type editing
Components may offer custom user interfaces for editing types, such as a date picker for date types, or an extended color picker for colors. Gone are the days when the property browser determined what types it supported. This task is now up to the components for custom types. The framework itself offers the facilities for editing all the basic built-in types. - Extensible views
Also known as property tabs, components can add views beyond the standard properties and events view available for .NET components on the design surface. - Reusable component
The Visual Studio .NET property browser is made up primarily of the System.Windows.Forms.PropertyGrid control, which can be reused in your applications.
Clearly, there is much to explore about the .NET property browser. This article will get you started in taking advantage of each of these great new features.
The Basics: Using Attributes to Customize Browsing
The basic mechanism for controlling browsing is analogous to that used in IDL-defined COM components, and that is adding metadata attributes. The most basic attribute for controlling browsing is the BrowsableAttribute. By default, the property browser displays all the public, readable properties defined on an object, and then places them into a miscellaneous category called "Misc." The following is a sample of a simple component:
public class SimpleComponent : System.ComponentModel.Component
{
private string data = "(none)";
private bool dataValid = false;
public string Data {
get {
return data;
}
set {
if (value != data) {
dataValid = true;
data = value;
}
}
}
public bool IsDataValid {
get {
// perform some check on the data
//
return dataValid;
}
}
}
Here is how that component would look in the property browser:
Figure 1. A simple component in the property browser
In this example, SimpleComponent has two properties: Data and IsDataValid. It makes little sense to have IsDataValid in the property browser because it is read-only and a developer does not need to know its value at design time. So, we can easily hide it by adding the BrowsableAttribute:
[Browsable(false)]
public bool IsDataValid {
get {
// perform some check on the data
//
return dataValid;
}
}
The C# compiler automatically appends the word "Attribute" to the attribute class names so that we can omit it. However, typing [BrowsableAttribute(false)] will also work. For attributes that are not explicitly specified on a property or class, the property is assumed to have the attribute's default value. In this case, the default value of BrowsableAttribute is true. This is also the case for code written in Visual Basic .NET. The only real difference is that Visual Basic .NET code uses angle braces ('<' and '>') around the attributes instead of the square brackets shown above.
Also, notice that the value abc in Figure 1 is bolded. A bolded value signifies that the value has been changed from its default and that the value will be persisted when code is generated for form or control in the designer. There is no reason to persist the value of a property that is still set to the default —this would only add time to the initializing of the component at startup, and generate more code in the file. Yet how would this SimpleComponent tell the property browser what the "default" value should be so the value will not always be persisted to code? For this purpose, you can use the DefaultValueAttribute, which takes an object in its constructor so that any type of value can be passed to it. When the property browser displays a value, it compares the current value with the value from DefaultValueAttribute and renders the text in bold if the values differ. In this case, the value will be bold when it has any value other than "(none)."
[DefaultValue("(none)")]
public string Data {
// . . .
}
You can determine whether your property requires more sophisticated logic than simply a stock value by adding a method to your component. The name of the method should start with ShouldSerialize followed by the property name, and the method should return a Boolean. In this case, the method would be called ShouldSerializeData*.* Adding the method below to SimpleComponent would act as the equivalent to adding the DefaultValueAttribute to the Data property, but it allows the decision to be made on demand.
private bool ShouldSerializeData()
{
return Data != "(none)";
}
It is often much easier for users to navigate properties that are categorized. As the name implies, the CategoryAttribute handles this task. It simply takes a category name string and the property browser will group properties by category name. You can supply any category name you like.
[DefaultValue("(none)"), Category("Sample")]
public string Data {
// . . .
}
One task that component developers commonly want to perform is the localization of their category strings. If you look at the class System.ComponentModel.CategoryAttribute, you will notice that its called GetLocalizedString method allows just this ability. To create a localized category string, you must define your own derivation of category attribute. This example looks in the manifest resources of the component for the category names based on a key. The key is used instead of specifying the actual category name when the attribute is applied to the property. The first time this attribute is queried for its category string, the GetLocalizedString override will be invoked and will receive that key value. The value that is returned will be then be displayed in the property browser as the category name.
internal class LocCategoryAttribute : CategoryAttribute {
public LocCategoryAttribute(string categoryKey) :
base(categoryKey){
}
protected override string GetLocalizedString(string key) {
// get the resource set for the current locale.
//
ResourceManager resourceManager =
new ResourceManager();
string categoryName = null;
// walk up the cultures until we find one with
// this key as a resource as our category name
// if we reach the invariant culture, quit.
//
for (CultureInfo culture =
CultureInfo.CurrentCulture;
categoryName == null &&
culture != CultureInfo.InvariantCulture;
culture = culture.Parent) {
categoryName = (string)
resourceManager.GetObject(key, culture);
}
return categoryName;
}
}
To use this attribute, define a resource with a key and put this attribute on your properties.
[LocCategory("SampleKey")]
public string Data {
// . . .
}
When you select multiple components on the design surface, the property browser displays the intersection, or "merge," of the properties on those components based on the property name and type. Then, when a property value is modified, all selected components receive that value for the selected property. It makes little sense to include some properties in a merge with others. Generally, such properties have values, such as a component's name, that must remain unique. Since setting a value when multiple components are selected attempts to change all of the properties to the same value, it is useful to prevent those properties from being included in that merge. The MergablePropertyAttribute solves this problem. Simply add this attribute with the value false to a property, and the property will be hidden when multiple components are selected on the design surface.
Some properties can affect the value of other properties. On data-bound components, for example, clearing the DataSource property implicitly clears the DataMember property that specifies the data row to which the property is bound. The RefreshPropertiesAttribute handles this case. Its default value is "None," but if another attribute value is specified, the property browser automatically refreshes when a value is changed that has this attribute. The other two such values are Repaint, which specifies that the property browser should re-query all the properties for their values and repaint them, and All, which means the object itself should be requeried for its properties. Use All if the change causes properties to be added or removed, but note that this is a more advanced usage scenario and is slower than a mere repaint. RefreshProperteis.Repaint is appropriate for the vast majority of cases. Remember to put this attribute on the property that causes the change when its value is modified, and not the property that is affected by the change.
Finally, DefaultPropertyAttribute and DefaultEventAttribute are class-level attributes that specify which property or event the property browser should initially highlight for a class. When selection changes between components, the property browser attempts to select a property of the same name and type as was selected on the prior component. If such a property cannot be selected, the property specified in the DefaultPropertyAttribute is selected. In the Events view of the property browser, the value from the DefaultEventAttribute is used. Also note that this is the event for which a handler will be generated when you double click the component on the designer.
Expandable Properties and String Conversion: TypeConverters and the Property Browser
One of the great features of the Visual Studio .NET property browser is the ability to display nested properties, allowing for a more granular and logical level of grouping than categories. Nested properties are also available in both categorized and alphabetical sort mode. It helps keep property lists compact—instead of both a Left and Top property, just a Location property that is expandable into X and Y will do for a separate entry.
Figure 2. Nested properties
But what determines whether or not a property is expandable? The answer is not in the property itself, but rather in the Type of property. In the .NET Framework, each type has associated with it a TypeConverter. The TypeConverter for types such as Boolean or string prevents them from being expandable in the property browser. It would make little sense to have sub-properties for type Boolean!
TypeConverters actually perform several functions within the .NET Framework and particularly within the property browser. As its name suggests, TypeConverters provide a standard way of dynamically converting values from one Type to another. The property browser, for example, only works with strings directly, so it relies on TypeConverters to take those string values and convert them to the Type that the property expects, and vice versa. TypeConverters also control expandability and allow complex types to work seamlessly with the property browser.
For example, imagine a Type called Person that looks like the following:
[TypeConverter(typeof(PersonConverter))]
public class Person {
private string firstName = "";
private string lastName = "";
private int age = 0;
public int Age {
get {
return age;
}
set {
age = value;
}
}
public string FirstName {
get {
return firstName;
}
set {
this.firstName = value;
}
}
public string LastName {
get {
return lastName;
}
set {
this.lastName = value;
}
}
}
Notice that the TypeConverterAttribute has been applied to the class, specifying the TypeConverter that will be used for this type. If no TypeConverterAttribute is specified, the default TypeConverter will be selected, but it won't do much. In the case of PersonConverter, we've overridden the GetPropertiesSupported and GetProperties methods of TypeConverter to allow the Type to be expandable.
internal class PersonConverter : TypeConverter {
public override PropertyDescriptorCollection
GetProperties(ITypeDescriptorContext context,
object value,
Attribute[] filter){
return TypeDescriptor.GetProperties(value, filter);
}
public override bool GetPropertiesSupported(
ITypeDescriptorContext context) {
return true;
}
}
This operation is common enough that the .NET Framework includes a TypeConverter derivation called ExpandableObjectConverter which does exactly this code. For easy expandability, just derive your TypeConverter from ExpandableObjectoConverter. We can now modify the TypeConverter to allow it to convert a Person type to and from a string.
internal class PersonConverter : ExpandableObjectConverter {
public override bool CanConvertFrom(
ITypeDescriptorContext context, Type t) {
if (t == typeof(string)) {
return true;
}
return base.CanConvertFrom(context, t);
}
public override object ConvertFrom(
ITypeDescriptorContext context,
CultureInfo info,
object value) {
if (value is string) {
try {
string s = (string) value;
// parse the format "Last, First (Age)"
//
int comma = s.IndexOf(',');
if (comma != -1) {
// now that we have the comma, get
// the last name.
string last = s.Substring(0, comma);
int paren = s.LastIndexOf('(');
if (paren != -1 &&
s.LastIndexOf(')') == s.Length - 1) {
// pick up the first name
string first =
s.Substring(comma + 1,
paren - comma - 1);
// get the age
int age = Int32.Parse(
s.Substring(paren + 1,
s.Length - paren - 2));
Person p = new Person();
p.Age = age;
p.LastName = last.Trim();
.FirstName = first.Trim();
return p;
}
}
}
catch {}
// if we got this far, complain that we
// couldn't parse the string
//
throw new ArgumentException(
"Can not convert '" + (string)value +
"' to type Person");
}
return base.ConvertFrom(context, info, value);
}
public override object ConvertTo(
ITypeDescriptorContext context,
CultureInfo culture,
object value,
Type destType) {
if (destType == typeof(string) && value is Person) {
Person p = (Person)value;
// simply build the string as "Last, First (Age)"
return p.LastName + ", " +
p.FirstName +
" (" + p.Age.ToString() + ")";
}
return base.ConvertTo(context, culture, value, destType);
}
}
Now our TypeConverter is working pretty well: it is expandable and allows the user to manipulate this type either as sub-properties or as a single string.
Figure 3. Expandable TypeConverter
To use the property as shown above, we can create a UserControl and paste in the following property code:
private Person p = new Person();
public Person Person {
get {
return p;
}
set {
this.p = value;
}
}
Editing and Displaying Custom Types
Property editing in the property browser can be done in one of three ways. First, the property can be edited as a string in place, and TypeConverters can do the job of converting that value to and from a string (if needed). Second, a drop-down arrow can cause editing UI to be displayed below the property. Finally, an ellipsis button can open some other method of custom UI such as a file dialog or font picker. We have already covered string editing, so first we will look at the case of a drop-down editor.
The .NET Framework contains several examples of drop-down editing. Control's AccessibleRole, Dock, and Color properties demonstrate what can be done with a drop-down editor.
Figure 4. Drop-down editor
TypeConverters also perform the job of simple drop-down editors. If you look at the documentation for TypeConverter, you will see three more virtual methods to accomplish this: GetStandardValuesSupported(), GetStandardValues(), and GetStandardValuesExclusive(). Using these methods, you can provide a list of predefined values for your property. In fact, it is a TypeConverter that allows the property browser to display Enum values in a drop-down (as in the left most example graphic). The property browser itself has no code to deal with Enum types specifically; rather, it uses TypeConverter functionality only.
For example, imagine a component called "FamilyMember" with a property called Relation on which a user could select how a person is related to another. To make this easier, the property has a drop-down editor filled with the most common values, such as mother, father, daughter, and sister. Given the vast number of possible relations, it is also desirable to allow a user to type in a custom string.
public class FamilyMember : Component {
private string relation = "Unknown";
[TypeConverter(typeof(RelationConverter)),
Category("Details")]
public string Relation {
get { return relation;}
set { this.relation = value;}
}
}
internal class RelationConverter : StringConverter {
private static StandardValuesCollection defaultRelations =
new StandardValuesCollection(
new string[]{"Mother", "Father", "Sister",
"Brother", "Daughter", "Son",
"Aunt", "Uncle", "Cousin"});
public override bool GetStandardValuesSupported(
ITypeDescriptorContext context) {
return true;
}
public override bool GetStandardValuesExclusive(
ITypeDescriptorContext context) {
// returning false here means the property will
// have a drop down and a value that can be manually
// entered.
return false;
}
public override StandardValuesCollection GetStandardValues(
ITypeDescriptorContext context) {
return defaultRelations;
}
}
But what about a more customized user interface? For this, we can use a class called UITypeEditor. UITypeEditor contains several methods that will be called by the property browser when rendering properties and when the user clicks a button for either a drop-down or pop-up editor.
Some property types such as Image, Color, or Font.Name paint a small representation of the value just to the left of the space where the value is shown. This is accomplished by implementing the UITypeEditor PaintValue method. When the property browser renders a property value for a property that defines an editor, it presents the editor with a rectangle and a Graphics object with which to paint. For example, we have a sample type called Grade for which we can paint a representation in the property browser. Our Grade class looks like the following:
[Editor(typeof(GradeEditor), typeof(System.Drawing.Design.UITypeEditor))]
[TypeConverter(typeof(GradeConverter))]
public struct Grade
{
private int grade;
public Grade(int grade)
{
this.grade = grade;
}
public int Value
{
get
{
return grade;
}
}
}
When we enter a grade in the property browser, we can show different bitmaps based on the value.
Figure 5. Entering a grade in the property browser
The code to make this happen is simple. Notice the EditorAttribute on the Grade type above, which references the class GradeEditor:
public class GradeEditor : UITypeEditor
{
public override bool GetPaintValueSupported(
ITypeDescriptorContext context)
{
// let the property browser know we'd like
// to do custom painting.
return true;
}
public override void PaintValue(PaintValueEventArgs pe)
{
// choose the right bitmap based on the value
string bmpName = null;
Grade g = (Grade)pe.Value;
if (g.Value > 80)
{
bmpName = "best.bmp";
}
else if (g.Value > 60)
{
bmpName = "ok.bmp";
}
else
{
bmpName = "bad.bmp";
}
// draw that bitmap onto the surface provided.
Bitmap b = new Bitmap(typeof(GradeEditor), bmpName);
pe.Graphics.DrawImage(b, pe.Bounds);
b.Dispose();
}
}
As mentioned above, UITypeEditors can also do pop-up or drop-down editors for property values. The sample included later in this article contains an example of this process. For more information, see the UITypeEditor.GetEditStyle and UITypeEditor.EditValue methods and the IWindowsFormsEditorService interface.
Providing Alternate Property Views
When you create a Windows Forms application in Visual C#™ .NET, you might notice an extra button on the toolbar of the property browser that looks like a lightning bolt. When you push that button, you are directed toward a property browser view that allows you to edit event handlers instead of properties. This is actually an extensible mechanism for developers.
The views in the property browser are referred to as property tabs, and consequently the primary class involved in adding the view is System.Windows.Forms.Design.PropertyTab. A property tab can be associated with a particular component, a designer document, or statically associated so that it is always available. Tabs that are related to a component or document are specified using the PropertyTabAttribute on a class. This attribute specifies the Type of the tab to create and how its visibility on the property browser should be managed via the PropertyTabScope parameter of PropertyTabAttribute. Tabs that have a scoped component will only be visible when the component that has the PropertyTabAttribute on it is visible. Document scoped tabs will remain visible for all designer objects on the current designer document. The default scope for a property tab is PropertyTabScope.Component.
For a sample of PropertyTabs, see the included FunkyButton project. FunkyButton is a UserControl that exposes a PropertyTab that enables you manipulate the shape of the button into a non-rectangular control.
Figure 6. FunkyButton
The currently selected property tab is where the property browser gets the properties for the selected object. Property tabs are thus given an opportunity to manipulate the set of properties that are displayed. The Events tab does this by returning events in a way that looks like properties. In this case, the property tab creates properties that represent the vertices of our control.
Properties in the .NET Framework are encapsulated by a class called PropertyDescriptor. PropertyDescriptor itself is an abstract base class —special. Derivations of it found in the Framework provide access to the normal properties that an object exposes. However, the property browser operates on these PropertyDescriptor objects rather than directly on properties, so we can create our own PropertyDescriptors that perform special tasks. In this case, we will create one to represent the number of vertices on our control, and then another to represent each of those vertices. Again, note that we adding entries to the property browser that do not correspond to actual properties on any object.
When the property browser asks a PropertyTab for properties, it calls the GetProperties method. For our sample PropertyTab, that method looks like this:
public override PropertyDescriptorCollection
GetProperties(ITypeDescriptorContext context, object component,
Attribute[] attrs)
{
// our list of props.
//
ArrayList propList = new ArrayList();
// add the property for our count of vertices
//
propList.Add(new NumPointsPropertyDescriptor(this));
// add a property descriptor for each vertex
//
for (int i = 0; i < ((FunkyButton)component).Points.Count; i++)
{
propList.Add(new VertexPropertyDescriptor(this,i));
}
// return the collection of PropertyDescriptors.
PropertyDescriptor[] props =
(PropertyDescriptor[])
propList.ToArray(typeof(PropertyDescriptor));
return new PropertyDescriptorCollection(props);
}
GetProperties just returns a collection of the property descriptors that we would like to return. The PropertyDescriptors themselves are fairly simple. Please take a look at the code samples to see how they are written.
The FunkyButton sample also demonstrates the implementation of a drop-down editor. For each Point vertex, instead of entering the standard X,Y Point values, the PropertyTab instead substitutes an editor that displays a representation of the FunkyButton shape and allows the placing the vertices graphically. This makes setting the button shapes much easier.
Figure 7. Placing the vertices graphically
Since the custom PropertyTab is providing the properties, it is also easy for it to override the editor for this property. Doing so simply involves overriding the GetEditor method of PropertyDescriptor and returning an instance of the custom editor.
public override object GetEditor(Type editorBaseType)
{
// make sure we're looking for a UITypeEditor.
//
if (editorBaseType == typeof(System.Drawing.Design.UITypeEditor))
{
// create and return one of our editors.
//
if (editor == null)
{
editor = new PointUIEditor(owner.target);
}
return editor;
}
return base.GetEditor(editorBaseType);
}
Designing the editor is easy as well. The editor is simply a UserControl, so we can go about designing it like any other type of WindowsForms object.
Figure 8. Designing the editor
Finally, when the user clicks the down arrow in the property browser, our editor will have an opportunity to create its user interface and drop it down. The override of UITypeEditor.EditValue in PointUIEditor handles this.
public override object EditValue(
ITypeDescriptorContext context,
IServiceProvider sp, object value)
{
// get the editor service.
IWindowsFormsEditorService edSvc =
(IWindowsFormsEditorService)
sp.GetService(typeof(IWindowsFormsEditorService));
// create our UI
if (ui == null)
{
ui = new PointEditorControl();
}
// initialize the ui with the settings for this vertex
ui.SelectedPoint = (Point)value;
ui.EditorService = edSvc;
ui.Target = (FunkyButton)context.Instance;
// instruct the editor service to display the control as a
// dropdown.
edSvc.DropDownControl(ui);
// return the updated value;
return ui.SelectedPoint;
}
And You Can Use It Too!
Finally, we've made the core of the property browser in Visual Studio .NET available to you for use in your applications. The control, called System.Windows.Forms.PropertyGrid, can be added to your toolbox in Visual Studio .NET by picking PropertyGrid from the .NET Framework Components tab of the Customize Toolbox dialog.
The PropertyGrid works like any other Windows Forms control. You can anchor or dock it in layout, change its colors, and so on. The following list shows some of the interesting properties specific to the PropertyGrid:
- SelectedObject
Determines the object for which the PropertyGrid will display properties. - ToolbarVisible
Shows or hides the Toolbar shown at the top of the PropertyGrid. - HelpVisible
Shows or hides the help text at the base of the PropertyGrid. - PropertySort
Determines the sort type for the PropertyGrid (categorized, alphabetical, etc.).
Each of these properties can be set normally at design time. At run time, however, the PropertyGrid can be used to manipulate objects that it is browsing. Below is an example of the PropertyGrid on a Form browsing a button. In this case, the PropertyGrid toolbar and help information have been hidden. As pointed out above, you can control this with properties on the PropertyGrid type itself.
Figure 9. PropertyGrid with hidden toolbar and Help information
Conclusion
The .NET Frameworks and Visual Studio .NET have added a great deal of functionality to the new version of the property browser. Since the property browser is at the core of developer's RAD experience, these features allow even more flexibility while maintaining the ease of use that made the property browser so popular in Visual Basic. With the ability to use the PropertyGrid in your applications, you can simplify your user interface designs and spend more time writing great applications and less time laying out user interface.