Creating Collection Controls with Rich Design Time Support, part 2

divil

Ultimate Contributor
Joined
Nov 17, 2002
Messages
2,746
Location
England
Controlling Serialization

Before any changes we make to the Buttons collection will be serialized to code, we need to add a TypeConverter class and associate it with ColourButton. A TypeConverter helps the serializers know how to recreate an object that is already instantiated. I'm going to use a very simple TypeConverter in this example, which simply tells the serializers to use the default, parameterless constructor.
<code available in full article>

We also need to tell the serializers that they have to go in to our Buttons property before they will even get that far, and we do this with the DesignerSerializationVisibilityAttribute class. Apart from its name being such an impressive length, all this attribute does it inform the serializers what to do with our property. We want them to delve in to the collection, so we specify Content.

When we add buttons to the control at design time, save, close the designer and re-open it, the buttons are there again. That's all we have to do with regards to serialization, and it's a big step out of the way.

Adding the Designer

Just before we add the designers, we'll clean up a couple of things. Firstly, we apply a BrowsableAttribute to the Buttons property, specifying False so the property doesn't appear in the propertygrid. Secondly, you may have noticed that when testing the control, adding buttons to the collection caused the buttons to appear in the component tray area of the form. This is normal since we are using components, but in this case we want to hide then. We do so using the DesignTimeVisibleAttribute class, again specifying False. Lastly, and this is a very minor detail, we use the ToolboxItemAttribute class to stop our ColourButton classes from appearing in their own right in the toolbox.

Now, we can go on to creating our designer. It will inherit from ControlDesigner. What we need it to do is handle clicks on the main control so that they select individual buttons, and listen for events on the design surface so we know when the user has selected something else. We also need to listen for an event fired when the user deletes one of the button components. Lastly, we need to override the AssociatedComponents property and simply pass it the Buttons collection, so it knows they go along with the control. It makes use of this information when the user copies the control to the clipboard and pastes it somewhere else.

This is a good time to tell you about the GetService function. The VS.NET IDE hosts a great deal of services, tied to a hierarchical chain of resources. They go up as high as the project level, and as low as a view of a particular source file (design view and code view). The services we are interested in are ISelectionService and IComponentChangeService. Every design view of a source file has these, and we can access them through the protected GetService method of the ComponentDesigner class, which ControlDesigner inherits from.

Designers have an Initialize function, which is called pretty much immediately after they are created. This function accepts a parameter which contains the object the designer is to provide support for. It is in this function that we will get a hold of ISelectionService and IComponentChangeService and wire up the events we need, which are SelectionChanged and ComponentRemoving. It is important to remember to unwire the events, which we do by overriding the Dispose method.

It's important to note that when writing designers, things can go wrong. They certainly have for me. Because designers are integrated quite tightly with the host environment, if you code something wrong or forget to clean up after yourself, things can really go awry. The kind of things it takes a restart of the IDE to fix. This is referred to as "playing nice with the other designers". Heaven forbid you should cause an exception to be thrown in designer code - debugging them is a real pain.

Anyway - here's the code to start off our designer. I've also tied the designer to the main control by using the DesignerAttribute class. This designer does nothing apart from wiring up the events we need and calling an internal function (with no code as yet) in the main control.
<code available in full article>

Adding Buttons

The first thing we want the user to be able to do is to buttons. We will make use of a designer verb to do this. For an explanation of designer verbs, see my article "Introduction to Designers". We only want one verb, and we'll simply title it "Add Button".

In the code that executes when the user activates this verb, we have to create a button and add it to the collection. This may sound trivial, but this is one of those times when we have to play nice with the other designers. If we simply created a button and added it to the collection, how would the IDE know anything had changed? How would it know WHAT had changed, so the user can undo/redo?

Enter the DesignerTransaction class. When you perform a significant action (or group of actions) to something on the design surface, you should wrap it in a transaction. Every transaction has a friendly name, which appears on the dropdown by the Undo/Redo buttons in the host environment. Also, every distinct change to make to an object (in this case, the Buttons collection) needs to be wrapped with a call to OnComponentChanging and OnComponentChanged, on the IComponentChangeService.

Lastly, you should not attempt to instantiate a ColourButton directly - let the designer host (another service we'll use) do the creating for you. This ensures that the object is on the design surface, and it keeps everyone happy. If the ColourButton class had a designer itself, that would get created too. I know this all sounds like a lot of work, and it is, but you get used to it and most of it is boilerplate that can be copy/pasted easily.
<code available in full article>

Note that even after writing all that, our implementation isn't quite complete yet - you can add buttons, and the undo and redo buttons will remove the button from the design surface ok but they won't remove the button from the Buttons collection - we'll come back to that later.

Selecting Buttons

Designers offer a useful method to override, called GetHitTest. This is passed some coordinates, and it's up to your logic to let the designer know whether or not to pass the event (usually a click) on to the control underneath. We will override this method, and see if the mouse cursor is within the bounds of any of the buttons on the control. If it is, we'll return true.
<code available in full article>

This way, our MouseDown event in the control will be fired if the user clicks on a button. In this event, we check if we're in design mode (with the DesignMode property) and if we are, find which button the cursor is on. Then we get a reference to ISelectionService and set the selection to that button.
<code available in full article>

At this point, clicking on an individual button in the control at design time will select it, and you can even modify its properties in the propertygrid. We've one last piece of code to write before the selection stuff is complete though, and that's filling in the function we created earlier that is called when the selection changes. It's in here that we'll set the highlightedButton variable we created so the selection is indicated visually too.
<code available in full article>

We're almost there. We can now add the control to a form, use the designer verb to add buttons, and select those buttons visually, changing their properties in the propertygrid.

Removing Buttons

This is another question of playing nicely with the designers. We will be writing the code to go in the OnComponentRemoving function in our designer. We need to handle two things in here. Firstly, the user removing the main control. When this happens we need to destroy all the buttons that are on the design surface. Secondly, when the user removes a button by selecting it and pressing delete. We need to remove it from the Button collection when this happens. Again, any changes we make to anything need to be wrapped in OnComponentChanging and OnComponentChanged calls.
<code available in full article>

Now that we've added that code, the user can delete buttons visually as they would delete any other control or component on the design surface. Also, Undo and Redo now work when adding buttons.

Conclusion
We have created the basics of a toolbar control with rich design time support. Adding more properties to the buttons is easy compared to the code we've had to write to enable this support. I hope you've found this article useful, it has certainly demonstrated a lot of the techniques you'll use when writing both design time and runtime code. The toolbar doesn't actually do anything at run time except sit there and look pretty, but we already have the elements in place to add support for mouseovers and a ButtonClick event.

I have provided a solution with both a VB and C# project, which are functionally identical.

View Full Article (with download)
 
Back
Top