Due to the size of this article, I've had to split it over two posts and remove the embedded code examples entirely. The full article is available here.
What are Collection Controls?
Actually it's a term I just made up, but when I say it I am referring to those user interface controls that present themselves as lists. The more obvious examples of these are listboxes, listviews and treeviews. Less obvious are things like toolbars. They all maintain a collection of objects which are displayed.
In some cases, these objects in the collection have collections of subitems themselves. An example of this is the listview, where each item can have subitems when the control is in Report view mode.
When you introduce a collection to your control, your job suddenly gets a lot harder. You end up having to write at least three more classes than you would if you were just developing a simple control with properties, such as a Button.
Requirements of a Collection Control
When writing one of these controls, it is usual to spend a little more time in code when you are still writing the object model. You have to define the property on the main control used to access the collection. You have to write the class which will represent each individual item (for example, the ListViewItem class). You have to write the class which will act as a collection for your subitems. And that's just to get it functional.
To add design time support, you have to write a class to act as a Type Converter for your subitems. When the user has populated your control at design time, the code serializers go through each object in your collection and use this converter class to inspect it and give it the best way of recreating it (i.e. which constructor to use).
Although this type of control has subitems, they are typically not responsible for drawing themselves. They do not actually have windows of their own, instead it is up to the parent control to calculate their positions and draw them.
Rich Design Time Support
Is another term I've invented. I use it to refer to doing that little bit of extra work to really make your control easy to work with at design time. I have a couple of my own controls posted, and neither of them use the Collection Editor which is the standard way of modifying collections at design time. Instead, they use a system of designer verbs and selections to make the changes visually.
To add rich design time support, you will likely be writing a designer for the main control, and a designer for the subitems. It's in these designers and in extensions of the code in your control that you will add the necessary code.
One of the requirements is that your subitems are selectable and modifiable with the usual property grid control. To enable this, every subitem must be present on the design surface. This means that it has to implement the IComponent interface, and the easiest way to do that is to derive them from Component.
The beauty of rich design time support is the user being able to select each subitem just by clicking on it. It's not a trivial task, and as far as the designer is aware (by default) you're just clicking on part of the main control. It's up to our design time code to use the interfaces provided by the host environment to select the subitem the user has clicked on and draw it as such.
We also need to listen to selection change events from the host environment, so that when the user selects a different control, we are notified and can redraw.
Designing the Object Model
For this article we will create a control which is laid out like a toolbar. All the "buttons" will have a Colour property which will be the only way of controlling their appearance. The buttons will be selectable and modifiable at design time.
The main control will feature only one custom property, which we will call "Buttons". We will hide this property at design time using the BrowsableAttribute class, because we want to use our own logic to add and remove them, rather than the collection editor.
Our subitems, which we will call ColourButtons, will have just one property - Colour. When a button is selected, a thick border will be drawn around it. I know this is a pretty useless control we're developing, but you would use exactly the same method to develop any advanced control, such as a toolbar or a list of some kind.
One of the most important things to get right with a control like this is separating the layout logic from the drawing logic. Internally, the control needs to keep a list of rectangles maintained, one for each button. We will implement a CalculateLayout function that loops through the collection and generates the rectangles. This function will be called whenever a button is added to or removed from the collection, or the main control is resized.
The drawing code is much easier if all the rectangles are pre-calculated like this. You should never calculate positions in drawing code, because it just isn't necessary. Drawing is required far more of the time than calculating positions.
Starting Off
I won't put all the code in to this article, because it would just get cluttered. Instead I will paste the important bits, and attempt to describe the rest. I will be developing the control in both VB and C# as I write, and the resultant solution will be available for download at the end.
First things first, we add the new usercontrol to our project. As we don't want our drawing to flicker, we use the protected SetStyle function in the constructor to turn on the DoubleBuffer and AllPaintingInWmPaint styles. These two go hand in hand. We also define the CalculateLayout function, which we will be calling from the collection and when the control is resized.
Next comes defining the subitem class, and the strongly-typed collection class which we'll use to contain the buttons. At this point we add the Buttons property to the main control, which exposes a private instance of this collection, instantiated in the main control's constructor. The ColourButton has an internal Bounds member of type Rectangle, which will hold the position of the button in the control.
For simplicity, our collection will only implement the Add and Remove functions, and the indexer. Normally you would add a few more strongly-typed helper functions to it, such as IndexOf. The constructor of the collection is internal and takes an instance of the main control as a parameter. This is so that this instance can be passed on to buttons as they are added, because when the user changes the colour of a button it needs to signal that a redraw is needed. Here is the code for the ColourButton and ColourButtonCollection classes:
<code available in full article>
Drawing and Layout Logic
We have already created the CalculateLayout function (although it is blank at this point) and are calling it when buttons are added to or removed from the collection. We also need to override OnResize and call it there. For this example control we will display the buttons in one horizontal line, from left to right. We will leave some padding at the sides, then the buttons will take up the rest of the space vertically and make themselves as wide as they are tall.
The CalculateLayout function will also invalidate the control. Although you often redraw without calculating positions, you never calculate positions without redrawing.
<code available in full article>
Next is the drawing code, which for this example is incredibly simple. We override the OnPaint method to draw the buttons, simply filling their rectangles with a brush we create from their defined colour.
Note that there is another method, OnPaintBackground, which we do not touch. If we were doing anything special with the background of the control, like a different colour, we would. As it is, if we leave it we don't have to worry about painting the background at all. In fact since we're inheriting from UserControl our control already features a BackColor property and even a way to have an image as the background.
<code available in full article>
Note that I've introduced a variable scoped to the control to contain a reference to the button which should have a highlight drawn on it, if any. This will be important later when we deal with the user selecting buttons as design time. At this point, the control actually works. Since I haven't hidden the Buttons property from the propertygrid yet, after adding the control to a form I can go in to the collection editor and add buttons to it. The buttons all show up as white squares, but we're well on our way.
Continued in next post
What are Collection Controls?
Actually it's a term I just made up, but when I say it I am referring to those user interface controls that present themselves as lists. The more obvious examples of these are listboxes, listviews and treeviews. Less obvious are things like toolbars. They all maintain a collection of objects which are displayed.
In some cases, these objects in the collection have collections of subitems themselves. An example of this is the listview, where each item can have subitems when the control is in Report view mode.
When you introduce a collection to your control, your job suddenly gets a lot harder. You end up having to write at least three more classes than you would if you were just developing a simple control with properties, such as a Button.
Requirements of a Collection Control
When writing one of these controls, it is usual to spend a little more time in code when you are still writing the object model. You have to define the property on the main control used to access the collection. You have to write the class which will represent each individual item (for example, the ListViewItem class). You have to write the class which will act as a collection for your subitems. And that's just to get it functional.
To add design time support, you have to write a class to act as a Type Converter for your subitems. When the user has populated your control at design time, the code serializers go through each object in your collection and use this converter class to inspect it and give it the best way of recreating it (i.e. which constructor to use).
Although this type of control has subitems, they are typically not responsible for drawing themselves. They do not actually have windows of their own, instead it is up to the parent control to calculate their positions and draw them.
Rich Design Time Support
Is another term I've invented. I use it to refer to doing that little bit of extra work to really make your control easy to work with at design time. I have a couple of my own controls posted, and neither of them use the Collection Editor which is the standard way of modifying collections at design time. Instead, they use a system of designer verbs and selections to make the changes visually.
To add rich design time support, you will likely be writing a designer for the main control, and a designer for the subitems. It's in these designers and in extensions of the code in your control that you will add the necessary code.
One of the requirements is that your subitems are selectable and modifiable with the usual property grid control. To enable this, every subitem must be present on the design surface. This means that it has to implement the IComponent interface, and the easiest way to do that is to derive them from Component.
The beauty of rich design time support is the user being able to select each subitem just by clicking on it. It's not a trivial task, and as far as the designer is aware (by default) you're just clicking on part of the main control. It's up to our design time code to use the interfaces provided by the host environment to select the subitem the user has clicked on and draw it as such.
We also need to listen to selection change events from the host environment, so that when the user selects a different control, we are notified and can redraw.
Designing the Object Model
For this article we will create a control which is laid out like a toolbar. All the "buttons" will have a Colour property which will be the only way of controlling their appearance. The buttons will be selectable and modifiable at design time.
The main control will feature only one custom property, which we will call "Buttons". We will hide this property at design time using the BrowsableAttribute class, because we want to use our own logic to add and remove them, rather than the collection editor.
Our subitems, which we will call ColourButtons, will have just one property - Colour. When a button is selected, a thick border will be drawn around it. I know this is a pretty useless control we're developing, but you would use exactly the same method to develop any advanced control, such as a toolbar or a list of some kind.
One of the most important things to get right with a control like this is separating the layout logic from the drawing logic. Internally, the control needs to keep a list of rectangles maintained, one for each button. We will implement a CalculateLayout function that loops through the collection and generates the rectangles. This function will be called whenever a button is added to or removed from the collection, or the main control is resized.
The drawing code is much easier if all the rectangles are pre-calculated like this. You should never calculate positions in drawing code, because it just isn't necessary. Drawing is required far more of the time than calculating positions.
Starting Off
I won't put all the code in to this article, because it would just get cluttered. Instead I will paste the important bits, and attempt to describe the rest. I will be developing the control in both VB and C# as I write, and the resultant solution will be available for download at the end.
First things first, we add the new usercontrol to our project. As we don't want our drawing to flicker, we use the protected SetStyle function in the constructor to turn on the DoubleBuffer and AllPaintingInWmPaint styles. These two go hand in hand. We also define the CalculateLayout function, which we will be calling from the collection and when the control is resized.
Next comes defining the subitem class, and the strongly-typed collection class which we'll use to contain the buttons. At this point we add the Buttons property to the main control, which exposes a private instance of this collection, instantiated in the main control's constructor. The ColourButton has an internal Bounds member of type Rectangle, which will hold the position of the button in the control.
For simplicity, our collection will only implement the Add and Remove functions, and the indexer. Normally you would add a few more strongly-typed helper functions to it, such as IndexOf. The constructor of the collection is internal and takes an instance of the main control as a parameter. This is so that this instance can be passed on to buttons as they are added, because when the user changes the colour of a button it needs to signal that a redraw is needed. Here is the code for the ColourButton and ColourButtonCollection classes:
<code available in full article>
Drawing and Layout Logic
We have already created the CalculateLayout function (although it is blank at this point) and are calling it when buttons are added to or removed from the collection. We also need to override OnResize and call it there. For this example control we will display the buttons in one horizontal line, from left to right. We will leave some padding at the sides, then the buttons will take up the rest of the space vertically and make themselves as wide as they are tall.
The CalculateLayout function will also invalidate the control. Although you often redraw without calculating positions, you never calculate positions without redrawing.
<code available in full article>
Next is the drawing code, which for this example is incredibly simple. We override the OnPaint method to draw the buttons, simply filling their rectangles with a brush we create from their defined colour.
Note that there is another method, OnPaintBackground, which we do not touch. If we were doing anything special with the background of the control, like a different colour, we would. As it is, if we leave it we don't have to worry about painting the background at all. In fact since we're inheriting from UserControl our control already features a BackColor property and even a way to have an image as the background.
<code available in full article>
Note that I've introduced a variable scoped to the control to contain a reference to the button which should have a highlight drawn on it, if any. This will be important later when we deal with the user selecting buttons as design time. At this point, the control actually works. Since I haven't hidden the Buttons property from the propertygrid yet, after adding the control to a form I can go in to the collection editor and add buttons to it. The buttons all show up as white squares, but we're well on our way.
Continued in next post