The PropertyGrid and Custom Verbs
[Update 20050510]: As spotted by s_orbit in the comments, there was a slight (well, pretty important, but simple code fix) bug when you supplied verbs that were already disabled. I've updated the download to include the fix, and while I was at it I added a second version of the sample, converted and ready to go for VS2005 (zero code differences, just made the solution and project files and stuff).
Download the code for this post here (318k).
The PropertyGrid is a pretty cool control to have around when you're writing WinForms apps. There's so many ways that it comes in useful - especially when you combine it with the power of .net reflection, as I've described (partially) in a previous post.
It's so useful that at work we've developed an ASP.net version that does almost all the stuff the 'real' one does, including verbs.
Verbs? You might have noticed them before, but they're not especially common, especially since Visual Studio itself is one of the very few places that I've seen them used. You can find them yourself in Visual Studio however. Create a new Form, and in the designer drop almost any random component (component, not control) on it. You'll end with the PropertyGrid looking something like this (I used an EventLog component):
See the little guy I've circled in red? Verbs let you associate link labels with the object currently displayed within the grid. When the label is clinked, some code gets run - pretty simple.
Have you ever tried to get them to appear in your own application however? It's not simple. I thought it would be worth finally sitting down and working out exactly what was needed to get them to work. First I opened up my trusty copy of Reflector and had a read through the decompile of how the PropertyGrid worked, and then I started experimenting. This is the (probably incomplete - watch this space for updates if I discover any - or more likely, if other people poke holes in what I've written :) fruits of my playing: a simple library that let's you host any verb you like, with your own custom actions executed as a result.
Disclaimer: While the sample I've provided does indeed work, what I haven't done is extensive testing yet. This means I've not tested yet to make sure that custom type descriptors and converters get called, that the default way of editing simple common types (dates, booleans, etc) still works and renders, nothing. I won't say 'Use At Your Own Risk' yet, but I will say that if you want to use the code here, you might want to do adequate testing to ensure it doesn't break the rest of your stuff. Also, I've not shown anyone else this code until writing up this post, no one has come along and pointed out big holes in my theory. If you see a big hole, feel free to rub my nose in it.
Whenever an object is placed within the PropertyGrid, the grid checks to see if that object supports the IComponent interface. If it does, it calls the GetSite() method, which returns an object that implements ISite. The ISite is queried for different services - the two that matter are IExtenderListService and IMenuCommandService.
I don't know why it absolutely has to have the IExtenderListService, but if you don't give it one, the verbs might render and work, but no properties do :) It's enough to have one that actually does nothing, and all is still happy.
The IMenuCommandService interface has many methods defined in it, but for the propertygrid what really matters is the Verbs property. This property returns a DesignerVerbCollection, filled with all the verbs that need to be rendered.
Simple once you know how, isn't it?
There's no way in hell I'm going to do this every time I want to show one verb, so I've written a few classes to take care of it all. There's a base class called 'VerbHost' which implements IComponent. There's an ISite, IExtenderListService and IMenuCommandService. It calls back on your own classes to get the list of verbs to display. Here's a sample class I wrote that uses it.
Imports System.ComponentModel
'A sample class that exposes a couple of properties and some verbs.
'Once a verb is invoked, the verb is disabled.
Public Class Sample
Inherits VerbHoster.VerbHost
#Region " Private Members "
Private msText1 As String
Private msText2 As String
Private moVerbCollection As VerbHoster.VerbList
#End Region
#Region " Constructors "
Public Sub New()
MyBase.New()
msText1 = "foo"
msText2 = "bar"
CreateVerbs()
End Sub
#End Region
#Region " Public Properties "
<Description("A sample string property")> _
Public Property Text1() As String
Get
Return msText1
End Get
Set(ByVal Value As String)
msText1 = Value
End Set
End Property
<Description("A sample string property")> _
Public Property Text2() As String
Get
Return msText2
End Get
Set(ByVal Value As String)
msText2 = Value
End Set
End Property
#End Region
#Region " Private Methods "
Private Sub CreateVerbs()
moVerbCollection = New VerbHoster.VerbList
Dim oVerb As VerbHoster.Verb
For i As Int32 = 1 To 3
oVerb = New VerbHoster.Verb
oVerb.Text = "Verb " & i.ToString
oVerb.CallBack = AddressOf VerbInvoked
moVerbCollection.Add(oVerb)
Next
End Sub
Public Overrides Function GetVerbs() As VerbHoster.VerbList
Return moVerbCollection
End Function
Private Sub VerbInvoked(ByVal sender As VerbHoster.Verb)
If Not sender Is Nothing Then
MsgBox(sender.Text)
sender.Enabled = Not sender.Enabled
End If
End Sub
#End Region
End Class
By inheriting from the base VerbHost, you need to override one method so that your verbs can be obtained. I'm creating 3 just to prove the point. When creating a verb you also need to provide a callback delegate for when the verb is invoked. In this sample, I'm MsgBox'ing the text of the verb, then disabling it immediately.
Instead of posting all the code involved in making this work, download my sample project and have a look. I'd appreciate any comments especially if I'm breaking any rules with the property grid itself.
Download the code for this post here (318k).