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. 30 Jan 2009 at 17:58
    Nice article, I'm writing a toolstrip type control and this helped me figure out how to remove the child component on deletion. I liked how you selected components in the control on mouse down, however, I had done it differently. Because my control was written in the compact framework, with the designer code being in the full framework. I couldn't select my component in the control code itself by checking for design mode. Therefore, I handled wndproc: protected override void WndProc(ref System.Windows.Forms.Message m) { // left mouse down if (m.Msg == WM_LBUTTONDOWN) { int data = m.LParam.ToInt32(); int y = (int)(data & 0xFFFF0000) >> 16; int x = (int)(data & 0x0000FFFF); Point pt = new Point(x, y); OptionStrip strip = (Control as OptionStrip); clickItem = strip.GetItemAt(pt); if (clickItem != null) return; } else if (m.Msg == WM_LBUTTONUP && clickItem != null) { int data = m.LParam.ToInt32(); int y = (int)(data & 0xFFFF0000) >> 16; int x = (int)(data & 0x0000FFFF); Point pt = new Point(x, y); OptionStrip strip = (Control as OptionStrip); if (strip.GetItemAt(pt).Equals(clickItem)) { ISelectionService ss = (ISelectionService)GetService(typeof(ISelectionService)); ArrayList list = new ArrayList(); list.Add(clickItem); ss.SetSelectedComponents(list, SelectionTypes.Primary); } clickItem = null; return; } I think your solution is simpler, and probably more solid, but this is necessary as the ISelectionService does not exist in the compact framework. However, it does seperate design time code.
  2. 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.

  3. 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..
  4. 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




  5. 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).

Tim Dawson

Related discussion

Related podcasts

  • More jQuery in ASP.NET

    In this episode Chris Brandsma, Rick Strahl, Dave Ward, Bertrand Le Roy, and Scott Koon conclude their discussion of Microsoft's jQuery in ASP.NET announcement1.This episode of the Alt.NET Podcast is brought to you by LLBLGen Pro, the most mature O/R mapper and code generator out there.Are ...

Events coming up

  • Dec 9

    GL.net Group Meeting - December 2009

    Gloucester, United Kingdom

    The beginning of this year holiday season will belong to mocks. Ronnie and Stephen will take us for a tour around exciting world of unit testing.

Want to stay in touch with what's going on? Follow us on twitter!