Click here to Skip to main content
15,879,535 members
Articles / Programming Languages / C#

Exposing Object Methods in the PropertyGrid Command Pane

Rate me:
Please Sign up or sign in to vote.
4.69/5 (8 votes)
9 Feb 2010CPOL6 min read 28.6K   861   25   8
A tutorial on the steps required to expose an object's methods as "hot commands" through the PropertyGrid control.
DesignerVerbSite.png

Table of Contents

Introduction

The PropertyGrid control provides an extremely flexible and extensible way to manipulate objects in an application's user interface. Providing the same powerful exposure to an object's methods would be extremely useful. Any number of object-specific commands could then be easily made available to the end user without involving special, hard-coded menus and buttons. The PropertyGrid control itself has many properties dealing with a "hot commands region" or "commands pane". But as with many of the PropertyGrid's extensibility mechanisms, this one has little or no documentation, making it virtually undiscoverable. But once the mechanism is revealed and conveniently packaged for reuse, it can be simply applied to expose object methods as commands. My goal in this article is to provide a short tutorial on the DesignerVerb mechanism required to implement this solution and to provide a reusable class to simplify future implementations.

Background

The PropertyGrid control is most at home in a visual design-time environment such as the Visual Studio Forms Designer. Forms act as containers where controls are represented by sites that blend the needs of the container and its components, providing the user with editing functionality not generally of interest at runtime. Many of the complex details of that Component Model are addressed in a rather interesting but long CodeProject article: Leveraging .NET Components and IDE Integration: UI AOP in an MVC use case. But most of our application user interfaces are not derived from the .NET Component Model or IDE integration, making the richness of that infrastructure unavailable to us. Fortunately we only need to implement a few methods on a couple of interfaces to do the job.

Solution Overview

We'll construct a lightweight DesignerVerbSite class which will emulate the sites of the Component Model and interface with the PropertyGrid. It will act as a site by implementing the ISite interface and reveal tagged methods on our prototype object as DesignerVerbs through the IMenuCommandService interface. Our prototype object will emulate a component by supporting the IComponent interface and give its DesignerVerbSite to the PropertyGrid. Note that our prototype object could act as its own site with its own implementations of ISite and IMenuCommandService. But factoring out the site functionality as a separate class provides reusability.

A Tour of the Code

Most of the members of the required interfaces are never called in the context of this tutorial. So those methods just throw the NotImplementedException. Our prototype object class, here called Dummy, implements the IComponent interface, which requires us to also implement IDisposable.

C#
public class Dummy : IComponent

The single interface member of interest for the prototype object is the Site property. We need only return an instance of our DesignerVerbSite which connects back to our prototype object. Note the BrowsableAttribute is set to false here so that this property, which is not of interest to our users, will not appear in the PropertyGrid.

C#
[Browsable(false)]
public ISite Site
{
	// return our "site" which connects back to us to expose our tagged methods
	get { return new DesignerVerbSite(this); }
	set { throw new NotImplementedException(); }
}

The DesignerVerbSite needs some way to identify which of our methods should be exposed. We certainly don't want the user to see all the internals. We reuse the BrowsableAttribute here to flag this as a method to expose. We could create a new attribute class specifically for this purpose, but this seems quite appropriate. It's interesting that this attribute was even designed to be applied to methods, since the PropertyGrid apparently doesn't look for it there. Our TestMethod will simply display a MessageBox showing the value of the Color property; an arbitrary property just to have something to see in the PropertyGrid.

C#
[Browsable(true)]
public void TestMethod()
{
	MessageBox.Show("TestMethod invoked: " + Color.ToString());
}

Our lightweight site class implements the IMenuCommandService and ISite interfaces.

C#
public class DesignerVerbSite : IMenuCommandService, ISite

Our implementation of ISite is unremarkable. Returning null for the Container property is sufficient. The boolean DesignMode property get accessor is called, but both false and true return values appear to work just fine.

C#
public IContainer Container
{
	// Returning a null Container works fine in this context
	get { return null; }
}

public bool DesignMode
{
	// While this *is* called, it doesn't seem to matter whether 
         // we return true or false
	get { return true; }
}

Implementing ISite requires us to implement IServiceProvider and its single method, GetService. The PropertyGrid calls this to get our IMenuCommandService interface, which is finally where the interesting work is accomplished!

C#
public object GetService(Type serviceType)
{
	if (serviceType == typeof(IMenuCommandService))
		return this;
	return null;
}

It is our implementation of the Verbs property of the IMenuCommandService interface that provides the PropertyGrid with the list of commands to display in its command pane. At this point you could, of course, provide any arbitrary commands and implementations of interest. But as our goal here is to expose the object's own methods, we use reflection to enumerate them and build a collection of verbs. We filter the methods and expose only those tagged with a true BrowsableAttribute. We create the DesignerVerbs to use the method name as the text to be displayed in the UI. If we desire to present more user-friendly names, we could also tag the prototype object's methods with a DisplayNameAttribute, fetch that attribute's string value here and pass that text to the DesignerVerb instead.

Finally we connect the DesignerVerb to our VerbEventHandler to handle the invocation of the verb when the link in the command pane is clicked.

C#
public DesignerVerbCollection Verbs
{
	get
	{
		DesignerVerbCollection Verbs = new DesignerVerbCollection();
		// Use reflection to enumerate all the public methods on the object
		MethodInfo[] mia = _Component.GetType().GetMethods
				(BindingFlags.Public | BindingFlags.Instance);
		foreach (MethodInfo mi in mia)
		{
			// Ignore any methods without a [Browsable(true)] attribute
			object[] attrs = mi.GetCustomAttributes
					(typeof(BrowsableAttribute), true);
			if (attrs == null || attrs.Length == 0)
				continue;
			if (!((BrowsableAttribute)attrs[0]).Browsable)
				continue;
			// Add a DesignerVerb with our VerbEventHandler
			// The method name will appear in the command pane
			Verbs.Add(new DesignerVerb(mi.Name, 
				new EventHandler(VerbEventHandler)));
		}
		return Verbs;
	}
}

The rubber meets the road in the VerbEventHandler which is called when our DesignerVerbs are invoked. The handler's sender parameter is the DesignerVerb itself. We use its text to identify the method to invoke on the prototype object. This implementation only works for methods without parameters. An advanced implementation could certainly provide a user interface to collect values for methods with parameters.

C#
private void VerbEventHandler(object sender, EventArgs e)
{
	// The verb is the sender
	DesignerVerb verb = sender as DesignerVerb;
	// Enumerate the methods again to find the one named by the verb
	MethodInfo[] mia = _Component.GetType().GetMethods
			(BindingFlags.Public | BindingFlags.Instance);
	foreach (MethodInfo mi in mia)
	{
		object[] attrs = mi.GetCustomAttributes
				(typeof(BrowsableAttribute), true);
		if (attrs == null || attrs.Length == 0)
			continue;
		if (!((BrowsableAttribute)attrs[0]).Browsable)
			continue;
		if (verb.Text == mi.Name)
		{
			// Invoke the method on our object (no parameters)
			mi.Invoke(_Component, null);
			return;
		}
	}
}

Using the Code

Following the example in the sample code, one could easily reuse the provided DesignerVerbSite class in their own projects to expose object methods to the PropertyGrid. The interface methods required of the object itself are trivial to implement and can be placed in key base classes in most systems to be shared by all the derived objects to be browsed in the PropertyGrid. Then just tag desired methods as Browsable and you're ready to roll!

There is one issue worth noting if you use the PropertyGrid.SelectedObjects property to browse multiple selected objects in the PropertyGrid. While the PropertyGrid will set a property on all objects with a single interaction, it will only invoke a verb on one of the many selected objects when clicked. To avoid confusion, I suggest setting the PropertyGrid.CommandsVisibleIfAvailable property to false when multiple objects are selected for browsing. (The Visual Studio Forms Designer appears to take this approach.)

More to Learn

This article presents only the necessary and sufficient implementation required to expose an object's methods in the PropertyGrid. But this control has significant versatility unknown to most developers that can make it attractive for use in more places than previously considered. Object tree structures, collections and containment can be presented in an expanding tree view without popup (tunnel) dialogs. User-friendly text can be displayed for property names and values. Non-existent properties can be represented. Specialized interfaces can be provided to manipulate values which can be displayed in unique ways. Some of these mechanisms are simple but difficult to discover and can be described in a tip or FAQ. Others, like the one presented here, may require somewhat more complicated implementations worthy of a complete article.

History

  • 9th February, 2010: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
From punched cards to iPhones is quite an evolution!

Comments and Discussions

 
QuestionAn easier way for simple cases Pin
SBendBuckeye3-Nov-15 3:46
SBendBuckeye3-Nov-15 3:46 
QuestionDesignerVerbSite.Name.get should return null Pin
jho196528-Jan-15 11:39
jho196528-Jan-15 11:39 
GeneralMy vote of 5 Pin
Kevin Whitefoot25-Jan-13 1:42
Kevin Whitefoot25-Jan-13 1:42 
GeneralMy vote of 5 Pin
jpatterson00919-Jun-11 12:21
jpatterson00919-Jun-11 12:21 
This
GeneralICustomTypeDescriptor Pin
xdinos13-Oct-10 9:59
xdinos13-Oct-10 9:59 
GeneralRe: ICustomTypeDescriptor Pin
Christopher Withington18-Jun-12 4:20
Christopher Withington18-Jun-12 4:20 
GeneralRe: ICustomTypeDescriptor Pin
scottfe18-Jun-12 9:39
scottfe18-Jun-12 9:39 
GeneralThanks Pin
Mihai Maerean15-Feb-10 21:06
Mihai Maerean15-Feb-10 21:06 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.