Collection Controls with Rich Design Time Support

Adding & Selecting Buttons

Adding Buttons

The first thing we want the user to be able to do is to add 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.

VB.NET

Public Overrides ReadOnly Property Verbs() As _
System.ComponentModel.Design.DesignerVerbCollection
    Get
        Dim v As New DesignerVerbCollection()
        'Commands to insert and add buttons
        v.Add(New DesignerVerb("&Add Button", AddressOf OnAddButton))
        Return v
    End Get
End Property
Private Sub OnAddButton(ByVal sender As Object, ByVal e As EventArgs)
    Dim button As ColourButton
    Dim h As IDesignerHost = DirectCast(GetService(GetType(IDesignerHost)), IDesignerHost)
    Dim dt As DesignerTransaction
    Dim c As IComponentChangeService = DirectCast(getservice(GetType _
    (IComponentChangeService)), IComponentChangeService)
    'Add a new button to the collection
    dt = h.CreateTransaction("Add Button")
    button = DirectCast(h.CreateComponent(GetType(ColourButton)), ColourButton)
    c.OnComponentChanging(MyControl, Nothing)
    MyControl.Buttons.Add(button)
    c.OnComponentChanged(MyControl, Nothing, Nothing, Nothing)
    dt.Commit()
End Sub

C#

public override System.ComponentModel.Design.DesignerVerbCollection Verbs
{
    get
    {
        DesignerVerbCollection v = new DesignerVerbCollection();
        // Verb to add buttons
        v.Add(new DesignerVerb("&Add Button", new EventHandler(OnAddButton)));
        return v;
    }
}
private void OnAddButton(object sender, System.EventArgs e)
{
    ColourButton button;
    IDesignerHost h = (IDesignerHost) GetService(typeof(IDesignerHost));
    DesignerTransaction dt;
    IComponentChangeService c = (IComponentChangeService)
    GetService(typeof(IComponentChangeService));
    // Add a new button to the collection
    dt = h.CreateTransaction("Add Button");
    button = (ColourButton) h.CreateComponent(typeof(ColourButton));
    c.OnComponentChanging(MyControl, null);
    MyControl.Buttons.Add(button);
    c.OnComponentChanged(MyControl, null, null, null);
    dt.Commit();
}

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.

VB.NET

Protected Overrides Function GetHitTest(ByVal point As System.Drawing.Point) As Boolean
    Dim button As ColourButton
    Dim wrct As Rectangle
    point = MyControl.PointToClient(point)
    For Each button In MyControl.Buttons
        wrct = button.Bounds
        If wrct.Contains(point) Then Return True
    Next
    Return False
End Function

C#

protected override bool GetHitTest(System.Drawing.Point point)
{
    Rectangle wrct;
    point = MyControl.PointToClient(point);
    foreach (ColourButton button in MyControl.Buttons)
    {
        wrct = button.Bounds;
        if (wrct.Contains(point))
            return true;
    }
    return false;
}

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.

VB.NET

Protected Overrides Sub OnMouseDown(ByVal e As System.Windows.Forms.MouseEventArgs)
    Dim button As ColourButton
    Dim wrct As Rectangle
    Dim s As ISelectionService
    Dim a As ArrayList
    If DesignMode Then
        For Each button In Buttons
            wrct = button.Bounds
            If wrct.Contains(e.X, e.Y) Then
                s = DirectCast(GetService(GetType(ISelectionService)), ISelectionService)
                a = New ArrayList()
                a.Add(button)
                s.SetSelectedComponents(a)
                Exit For
            End If
        Next
    End If
    MyBase.OnMouseDown(e)
End Sub

C#

protected override void OnMouseDown(System.Windows.Forms.MouseEventArgs e)
{
    Rectangle wrct;
    ISelectionService s;
    ArrayList a;
    if (DesignMode)
    {
        foreach (ColourButton button in Buttons)
        {
            wrct = button.Bounds;
            if (wrct.Contains(e.X, e.Y))
            {
                s = (ISelectionService) GetService(
                typeof(ISelectionService));
                a = new ArrayList();
                a.Add(button);
                s.SetSelectedComponents(a);
                break;
            }
        }
    }
    base.OnMouseDown(e);
}

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.

VB.NET

Friend Sub OnSelectionChanged()
    Dim button As ColourButton
    Dim newHighlightedButton As ColourButton = Nothing
    Dim s As ISelectionService = DirectCast(GetService(GetType _
    (ISelectionService)), ISelectionService)
    'See if the primary selection is one of our buttons
    For Each button In Buttons
        If s.PrimarySelection Is button Then
            newHighlightedButton = button
            Exit For
        End If
    Next
    'Apply if necessary
    If Not newHighlightedButton Is highlightedButton Then
        highlightedButton = newHighlightedButton
        Invalidate()
    End If
End Sub

C#


internal void OnSelectionChanged()
{
    ColourButton newHighlightedButton = null;
    ISelectionService s = (ISelectionService) GetService(typeof(ISelectionService));
    // See if the primary selection is one of our buttons
    foreach (ColourButton button in Buttons)
    {
        if (s.PrimarySelection == button)
        {
            newHighlightedButton = button;
            break;
        }
    }
    // Apply if necessary
    if (newHighlightedButton != highlightedButton)
    {
        highlightedButton = newHighlightedButton;
        Invalidate();
    }
}

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.

You might also like...

Comments

Contribute

Why not write for us? Or you could submit an event or a user group in your area. Alternatively just tell us what you think!

Our tools

We've got automatic conversion tools to convert C# to VB.NET, VB.NET to C#. Also you can compress javascript and compress css and generate sql connection strings.

“Weeks of coding can save you hours of planning.”