Library tutorials & articles

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.

Comments

  1. 23 May 2007 at 19:35

    GREAT ARTICLE¡¡¡  thank you very, very much, now i can finish my own control. 

    I could not save my own custom class object, NOW I CAN.

  2. 19 Mar 2007 at 09:08

    I've tried to compile the code I've downloaded but at run-time (placed in a form on another project) I don't see the buttons I've created at deign-time..

  3. 17 Jan 2007 at 19:46

    Is it safe to assume value (of complex type) of a property has instance descriptor converter for every component/control you dropped on design surface.

    Regards
    Phani




  4. 01 Jan 1999 at 00:00

    This thread is for discussions of Collection Controls with Rich Design Time Support.

Leave a comment

Sign in or Join us (it's free).

AddThis

Related podcasts

  • CodeCast Episode 4: State of .NET, IE8, ASP.NET MVC, and O'Reilly Media

    CodeCast Episode 4: State of .NET, IE8, ASP.NET MVC, and O'Reilly MediaHosts Ken Levy and Markus Egger discuss the new State of .NET events, IE8, ASP.NET MVC, followed by an interview from PDC with two editors from O'Reilly Media. More on ASP.NET MVC can be found at http://asp.net/mvc. Interview...

Related jobs

Events coming up

  • Dec 6

    Developing AJAX Web Applications with Castle Monorail

    London, United Kingdom

    Monorail is the model-view-controller engine of the Castle Project, bringing many of the best ideas of Ruby on Rails to the .NET world. In this talk, David De Florinier and Gojko Adzic show how Monorail makes it easy to develop .NET based AJAX applications, and how to use the Castle Project to build Web 2.0 applications effectively. Come to this session if you are a .NET web developer. Everyone is welcome!