Click here to Skip to main content
15,867,686 members
Articles / Programming Languages / C#

SplitButton a .NET WinForm control, Part 1

Rate me:
Please Sign up or sign in to vote.
4.26/5 (17 votes)
1 May 200713 min read 100.4K   4.6K   96   11
A WinForm SplitButton control you can drag onto your form design surface then add the items to the drop-down menu from the client of the SplitButton. The items include a text display to the user and an invocation method callback.

Prerequisites

I assume that you are familiar with the publish-subscribe pattern, the .NET way, through events. The publisher fires an event and the subscriber consumes that event. I also assume that you are familiar with the Invoke() method and InvokeRequired property. In a multithreading environment the UI thread is the only thread allowed to manipulate the controls that it creates. The running thread may use the InvokeRequired property to determine if a thread-context-switch is needed.

How to read the article

Your best way to read this article is on a dual monitor system where the article is displayed on one monitor and the accompanying code is displayed on the other monitor. If a dual monitor system is not available to you, then you may care to flip between the article and the accompanying code displays. After you finish reading the article, read the accompanying code on your own in its entirety, as there are aspects of the code I did not go through in this article. I expect that you will to be able to follow the code on your own—enjoy!

Introduction

When Sir Isaac Newton, the man who is considered the Father of classical physics, was asked how he succeeded in coming up with a simple set of laws that govern the motion of objects, Isaac replied that he stood on the shoulders of giants. Isaac, of-course was referring to Galileo Galilei, Johannes Kepler and others who laid the foundation for his own work. In a similar manner I have stood on the shoulders of a giant in order to come up with this control.

When I needed a SplitButton control I found an article in CodeProject entitled: SplitButton: an XP style dropdown split button, by Gladstone. The article took me halfway to where I needed to be. I needed to hide the internals of the control from the consumer of the SplitButton and provide a simple interface to the consumer. (I encourage you to take a look at Gladstone's article and demo.)

I would like to further emphasize that this article does not come to out-do Gladstone's article. On the contrary, I consider Gladstone's contribution as great work. He took a ContextMenuStrip control and attached it to a regular button; thereby creating a SplitButton in a simple and easy to understand way. I will not repeat his work here nor will I explain it, but I will build upon it. I will further encourage you, the reader, to improve upon my work, publish your contribution and give Gladstone and me due credit.

In order to make the button a self-contained button that follows the principles of object orientation, it needs two kinds of support mechanisms and interface sets:

  1. run time support which is covered in this article
  2. design time support which will be covered in a subsequent article

Nomenclature

When the user of the application clicks on a SplitButton, see Figure 1 below, we need to distinguish between clicking on the entire button, the Button-Part and the Split-Part.

Screenshot - image002.jpg
Figure 1

Broad strokes walk through

  • Clicking on the entire button. See Listing 1 (a reproduction of the OnClick() method). Where base.OnClick(e); is the code line responsible for the entire button being clicked. OnMouseUp(MouseEventArgs mevent) is called but is of little consequence to the "entire button clicked."
  • Clicking on the Button-Part, the left side where the text goes. Still fires the same base.OnClick() method indicating that the entire button was clicked. In addition the code fires the ButtonClick(this, e); event-handler exposing the fact that the button was clicked in the "Button Part." OnMouseUp(MouseEventArgs mevent) is called but is of little consequence to the button-part.
  • Clicking on the Split-Part will be firing both the OnClick(EventArgs e) indicating that the button was clicked and the OnMouseUp(MouseEventArgs mevent) which will show the ContextMenuStrip.

Also, take a look at the event properties of the SplitButtonDropDown, part of the SplitButton design surface (SplitButton.cs [Design] view). See Figure 2 below. The ItemClick event is responsible for the specific menu selection.

Screenshot - image004.jpg
Figure 2

Towards the end of this article we will look more closely at the ItemClicked event.

This is a good time to run the demo and see the events displayed in the window. Figure 3 is an example of the screen displaying the events when the button-part is clicked.

<Screenshot - image006.jpg
Figure 3

The structure of the SplitButton Control code

The SplitButton Control class is divided into the following regions:

  • Fields: A region housing private members variables, like state variable and private information.
  • Events: The SplitButton adds one event, ButtonClick, to the Button base class. This ButtonClick event is fired only when the SplitButton is clicked on the Button-Part. Events that we inherit from the Button base class as well as the ButtonClick event are public.
  • Construction: The constructor of the SplitButton is public.
  • Helper Methods: Private methods servicing the SplitButton functionality and help in organizing the code.
  • Properties Exposing States: Public properties exposing a change of state of the control by manipulating private variables declared in the region named Fields.
  • Overridable Methods: Protected methods used to override some of the Button control class methods. We have already seen the OnMouseUp(MouseEventArgs mevent) and the OnClick(EventArgs e) methods.
  • Additional Interface Methods: At the client's disposal. This is the tentacles through which a Client communicates its needs to the SplitButton.
  • Internal Events Handling: The heart of the engine handling the interface set of functions exposed in the previous region.

Event handling

Let's look at the OnClick() method, see Listing 1. The method has signature and body as follows:

C#
1   protected override void OnClick(EventArgs e)
2   {
3       base.OnClick(e);
4
5       if (!IsMouseInSplit())
6       {
7           EventFire(ButtonClick, e);
8       }
9   }
Listing 1

Which bears the need for explaining the unexpected function call (line 7): EventFire(ButtonClick, e); One would expect the following code in Listing 2 instead of the EventFire(ButtonClick, e) call.

C#
1   if (ButtonClick !=null)
2       ButtonClick(this, e);
Listing 2

The above code, displayed in Listing 2, is the customary pattern for handling a button click event (or any other event). Line 1 (of Listing 2 checks to see if any invocation methods are bound to the event handler and line 2 calls the invocation methods (all of them). However, in a multithreading environment we may experience a thread-context-switch after line 1 and before line 2. Now if the new running thread (after the thread-context-switch) subtracted (or added) an invocation method from the event delegate, like so:

C#
splitButton1.ButtonClick -= new System.EventHandler(splitButton1_ButtonClick);

Then the ButtonClick() EventHandler delegate in line 2 will be a different EventHandler than the one in line 1. Delegates are immutable and therefore adding or removing an invocation method from a delegate results in a new delegate instance.

So, if the operation within the second thread leaves the ButtonClick delegate devoid of invocation methods, then calling line 2 will result in an exception.

An excellent explanatory work covering this topic can be found in Juval Lowy's extraordinary book, entitled: Programming .NET Components, 2nd Edition. See the explanation starting in Chapter 6's section entitled "Publishing Events Defensively" and ending in Chapter 8.

A short synopsis of the explanation is as follows:

Listing 2 can be rewritten as that in Listing 3, in an attempt to avoid the potential problem from a thread-context switch:

C#
EventHandler tmp = ButtonClick;

if (tmp != null)
    tmp(this, e);
Listing 3

However, the .NET optimizing complier may convert the code in Listing 3 back to the already known to be bad code in Listing 2 (it is an optimizing compiler). Listing 4 can solve this problem.

Note that in Listing 4, the decoration of the method EventFire(): [MethodImpl(MethodImplOptions.NoInlining)] prevents the compiler from optimizing the function call away by inlining the EventFire(..) method and nullifying the effect we are trying to achieve.

C#
// ButtonClick is deffered to a method called from the publisher,
// OnClick() method
EventFire(ButtonClick, e);
.
.
.
// Helper method
[MethodImpl(MethodImplOptions.NoInlining)]
private void EventFire(EventHandler evntHndlr, EventArgs e)
{
    // Make sure that the handler has methods bound to it.
    if (evntHndlr == null)
        return;

    evntHndlr(this, e);
}
Listing 4

The above code in Listing 4 does not handle correctly the possibility that one of the invocation methods bound to evntHndlr may throw an exception. Listing 5 solves this issue.

C#
[MethodImpl(MethodImplOptions.NoInlining)]
private void EventFire(EventHandler evntHndlr, EventArgs ea)
{
    if (evntHndlr == null)
        return;

    foreach (Delegate del in evntHndlr.GetInvocationList())
    {
        try
        {
            evntHndlr.DynamicInvoke(new object[] { this, ea });
        }
        catch (Exception /*ex*/)
        {
            //
            // Eat the exception
            //
        }
    }
Listing 5

The above code in Listing 5 does not take into account the possibility that one of the methods may come from a section that requires a thread-context-switch. For example, code updating a control needs to run on the same UI thread that created the control. Hence the last evolution of the code is presented in Listing 6 below.

C#
[MethodImpl(MethodImplOptions.NoInlining)]
private void EventFire(EventHandler evntHndlr, EventArgs ea)
{
    if (evntHndlr == null)
        return;

    foreach (Delegate del in evntHndlr.GetInvocationList())
    {
        try
        {
            ISynchronizeInvoke syncr = del.Target as ISynchronizeInvoke;
            if (syncr == null)
            {
                evntHndlr.DynamicInvoke(new object[] { this, ea });
            }
            else if (syncr.InvokeRequired)
            {
                syncr.Invoke(evntHndlr, new object[] { this, ea });
            }
            else
            {
                evntHndlr.DynamicInvoke(new object[] { this, ea });
            }
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine(string.Format(
                "SplitButton failed delegate call. Exception  {0}",
                ex.ToString()));
        }
    }
}
Listing 6

Six iterations and now we have a robust EventFire(..) helper method.

Interface needed for run time support

There are two methods that I provided as the interface specific for the SplitButton:

  • Clear all the drop-down items, see the ClearDropDownItems().
  • Add a drop-down item, see AddDropDownItemAndHandle(string text, EventHandler handler).

See the "Additional Interface Methods" region in the accompanying code for the above mentioned methods. This list of two methods can easily be expanded to include more methods and functionality in order to:

  • Remove a drop-down item
  • Alter a drop-down item
  • Insert a drop-down item
  • And more.

I feel that adding menu items will be the most heavily used functionality and I expect that ClearDropDownItems() will be used rarely. I do not believe that the rest of the functionality will be used. However, I encourage you to enhance the control if you need the additional functionality.

Mechanism supporting the Add and Clear methods

The client cannot bind an invocation method to a menu drop-down item since the menu drop-down is created on the fly after the client clicks on the split-part of the button. Therefore, we need a different mechanism.

I have decided to use a Dictionary<> construct like so:

Dictionary<string, EventHandler> _dropDownsEventHandlers =
                                  new Dictionary<string, EventHandler>();

Where the key to this dictionary, the first generic type, is the text display of the drop-down menu item; and the EventHandler, the second generic type, is the delegate to which the invocation method is bound.

An immediate consequence of this choice is the fact that we cannot have two positionally distinct drop-down items that share the same display text and behave differently. So, for example, we cannot have a menu item in position 0 displaying the text: "this is a menu item" then a menu item in position 1 displaying the text: "this is a menu item" and expect them to be bound to two distinct event handlers. However, I consider it to be a good choice.

The above should make the adding functionality of a drop-down item, in the method AddDropDownItemAndHandle(), clear. See Listing 7. By the same token, ClearDropDownItems() should be clear as well. Please review the code in the accompanying code.

C#
#region Additional Interface Methods

public void ClearDropDownItems()
{
    SplitButtonDropDown.Items.Clear();
    _dropDownsEventHandlers = new Dictionary<string, EventHandler>();
}

public void AddDropDownItemAndHandle(string text, EventHandler handler)
{
    // Add item to menu
    SplitButtonDropDown.Items.Add(text);

    // Add handler
    if (! _dropDownsEventHandlers.ContainsKey(text))
        _dropDownsEventHandlers.Add(text, handler);
}

#endregion

Listing 7

For emphasis' sake note that we did not bind any method to the drop-down items. Instead we have bound it to our Dictionary<> construct. Therefore, we will need to come up with an alternative method to bind the drop-down items to whatever invocation methods are now stored in this _dropDownsEventHandlers Dictionary<>.

Handling the click event on the drop-down item

Let's review the steps in an event life cycle:

We have two players

  • The event publisher class
  • The event subscriber class

For the sake of this discussion let's have a simple example to follow, see Listing 8 below.

C#
01   public class Publisher
02   {
03       public event EventHandler SomeEvent;
04
05       public void FireEvent()
06       {
07           if (SomeEvent != null) // Step 2 -- Check
08               SomeEvent(this, EventArgs.Empty); // Step 3 -- call
09       }
10   }
11
12   public class Subscriber
13   {
14       public void OnSomeEvent(object sender, EventArgs e)
15       {
16           // Step 4 –- Do something useful
17       }
18   }
19
20   public class Controller
21   {
22       Publisher pub;
23       Subscriber sub;
24
25       public void Initializer()
26       {
27           pub = new Publisher();
28           sub = new Subscriber();
29           pub.SomeEvent += new EventHandler(sub.OnSomeEvent);
30       }
31
32       public void Doer()
33       {
34           pub.FireEvent();  // Step 1 -- trigger the event
35       }
36   }
Listing 8

Note that after my lengthy explanation in the section entitled Event handling as to the fact that the code in lines 7 and 8 of Listing 8 is wrong for a multithreading environment; I turn around and go against my own advice. This is not an oversight. I would rather keep the code as simple as possible for the sake of this discussion. Otherwise, the above code in lines 7 and 8 are inappropriate for a control that may be consumed by a multithreading environment client and these lines should be replaced with a function call to EventFire(..).

The event, in its lifecycle, will follow these milestones:

  • Something will trigger the event to fire. See line 34 in Listing 8.
  • The FireEvent() checks to see if there are any invocation methods subscribed to the event, see line 7.
  • The Publisher will invoke the Event Handler's invocation methods, line 8.
  • The Subscriber's function, OnSomeEvent() will be called, lines 14 - 17.

Now, we get to handle the click events from the drop-down, see within the accompanying code, you will find the SplitButtonDropDown_ItemClicked method:

C#
private void SplitButtonDropDown_ItemClicked(object sender,
    ToolStripItemClickedEventArgs e)
{
    //
    //     Close the drop down first
    //
    SplitButtonDropDown.Close();

    //
    // Translate the ItemClicked, event that was just fired by the
    // drop-down menu, to the event the user bound its handling
    // to in _dropDownsEventHandlers[<name of the drop down>]
    //
    string textDisplay = e.ClickedItem.Text;
    EventHandler adaptorEvent = _dropDownsEventHandlers[textDisplay];

    //
    // Fire the new event
    //
    EventFire(adaptorEvent, EventArgs.Empty);
}
  • The click on the menu item is the trigger that will cause the ContextMenuStrip to fire the ItemClicked event.
  • The ContextMenuStrip checks to see if any method is bound to the event
  • It finds the SplitButtonDropDown_ItemClicked() and therefore it invokes it.
  • So at this point we switch EventHandlers and call the appropriate EventHandler, one of the list stored in _dropDownsEventHandlers.

The signature of SplitButtonDropDown_ItemClicked() is as follows:

C#
private void SplitButtonDropDown_ItemClicked(
    object sender, ToolStripItemClickedEventArgs e)

The EventArgs derived class, ToolStipItemClickedEventArgs, contains the information as to which item was clicked. More importantly, it contains the display text of the drop-down item which we use as a key to retrieve the handler from the Dictionary<>.

Therefore, we can switch from handling the ItemClicked event and fire a secondary event as follows:

C#
string textDisplay = e.ClickedItem.Text;
EventHandler adaptorEvent = _dropDownsEventHandlers[textDisplay];
EventFire(adaptorEvent, EventArgs.Empty);

This concludes our handling of the click event from the button side. The consumer of our SplitButton will need to add its callback functionality.

From the consumer point of view

The form, Form1, in the demo project, during the load event, adds three drop-down items:

C#
splitButton1.AddDropDownItemAndHandle("Test 1", Test1Handler);
splitButton1.AddDropDownItemAndHandle("Testing Testing", Testing2Handler);
splitButton1.AddDropDownItemAndHandle("Testing testing testing",
    Testing3Handle);

These items are added to the drop-down list in the order that they are called.

Each method takes a signature of an EventHandler callback. These three EventHandler callback methods need to be defined and they are. For example Test1Handler is defined as follows:

C#
private void Test1Handler(object sender, EventArgs e)
{
    textBox1.Text += "Test 1 was fired" + Environment.NewLine;
}

This is very simple for the consumer; all the heavy lifting is done within the SplitButton control.

Moreover, if we decide to change the SplitButton control we may do so for as long as we do not change the interface to the client and the client will need no code change.

Room for improvement

  • Most of Microsoft's control can be operated without the use of a mouse. This control is geared to work entirely with the mouse. A potential improvement is to provide a set of keyboard clicks that are intuitive and thus achieve the same result as that of using the mouse.
  • The display of the ContextMenuStrip can improve to have the option of displaying a fixed width or variable width drop-down.
  • The display of the ContextMenuStrip can improve to allow the client to set programmatically the maximum number of items displayed in the drop-down.
  • A ContextMenuStrip allows ToolStripMenuItems, ToolStripSeparator, ToolStripTextBox and ToolStripComboBox. The SplitButton allows only ToolStripMenuItems, therefore extend SplitButton to allow everything that the ContextMenuStrip allows.
  • There is no indication, after the fact, which of the drop down options was selected (except for using the PersistDropDownName option). Therefore, improve the SplitButton control by coming up with a way to let the user know which option they chose.

These are some of the potential improvements and there are many more ways to improve upon this SplitButton. If you improve upon the current SplitButton, publish your work, and you will make the world a better place to live in.

Summary

We have achieved our goal of providing a self-contained button following the object oriented methodology that provides a simple and easy interface to its consumers. The developer may drag the SplitButton onto the design-surface, and add drop-down items from the consumer side, albeit programmatically.

Where do we go from here? We need to add design time support so the consumer, of the SplitButton, will be able to drag the button onto a design surface and in the properties window set the drop-down items.

Until next time--enjoy!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
United States United States
avifarah@gmail.com

Comments and Discussions

 
QuestionSometimes when press the arrow doen't trigger dropdown Pin
Valerii Nozdrenkov5-Feb-20 7:16
Valerii Nozdrenkov5-Feb-20 7:16 
AnswerRe: Sometimes when press the arrow doen't trigger dropdown Pin
Valerii Nozdrenkov5-Feb-20 8:40
Valerii Nozdrenkov5-Feb-20 8:40 
PraiseSince I'm stuck with WinForms... Pin
DrMcClelland9-May-16 7:17
DrMcClelland9-May-16 7:17 
GeneralRe: Since I'm stuck with WinForms... Pin
Avi Farah18-Dec-16 0:43
Avi Farah18-Dec-16 0:43 
Questiondon't download Pin
Member 1142353218-Feb-16 0:18
Member 1142353218-Feb-16 0:18 
Generalover engineered Pin
Kir Birger22-Jul-10 8:36
Kir Birger22-Jul-10 8:36 
GeneralGreat job! Pin
luuaks31-Jul-07 0:09
luuaks31-Jul-07 0:09 
GeneralRe: Great job! Pin
Avi Farah11-Oct-07 8:55
Avi Farah11-Oct-07 8:55 
Generalplease Avi can you help me in video processing Pin
maroq19-Apr-07 8:57
maroq19-Apr-07 8:57 
GeneralRe: please Avi can you help me in video processing Pin
Avi Farah20-Apr-07 1:22
Avi Farah20-Apr-07 1:22 
GeneralRe: please Avi can you help me in video processing Pin
The_Mega_ZZTer23-Apr-07 6:57
The_Mega_ZZTer23-Apr-07 6:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.