Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Broadcasting Events through a Control Hierarchy

0.00/5 (No votes)
3 Mar 2006 3  
Implement support for broadcasting events to all ancestors in a control hierarchy for Win Forms applications.

Introduction

When events occur in a Windows application, a set of registered handlers are invoked. Only the handlers that are directly registered with the control are invoked. However, there may be times when it is more desirable to have all the controls in a hierarchy handle a single event. For instance, suppose that a Panel has a set of buttons, all of which perform similar behavior. It might be desirable to have the Panel handle the Click event rather than having a separate handler for each button. This is not possible using native framework functionality.

This article discusses the EventBroadcastProvider which was created to solve the problem of relaying events through the control hierarchy. Using this class, a specific event can be monitored and relayed to all the handlers through the hierarchy.

Event broadcasting

Event broadcasting in the context of this article is not related to that which is supported by a MulticastDelegate, rather it refers to the ability for events raised in one control to be relayed up the control hierarchy to all ancestors, thereby giving them the ability to handle the event as well, all without coupling the child to its parent.

Consuming

One of the primary design constraints was for a simple system, something that can be consumed with little code or complications. Event broadcasting is handled by a single class, EventBroadcastProvider, which hooks the necessary events in all controls of the hierarchy (as discussed below). To add support for broadcasting, add the following class initialization to your code. Once initialized, all management of the events and relays are handled automatically. Out of sight, out of mind.

EventBroadcastProvider broadcastEvent =
     EventBroadcastProvider.CreateProvider(this.panel1, "Click");

As shown, this hooks the control panel1 as well as all its children and relays the Click event when raised.

Hooking control events

No direct support exists for broadcasting event. An event handler can only be invoked by the object that raised the event. In the image below, button1 and panel1 both have a Click event handler, but when the user clicks button1, only that handler is invoked, not the handler for panel1 even though button1 is a child control of panel1:

Likewise, if only panel1 has a handler, then no handler is invoked when button1 is clicked:

To support broadcasting, all the events within the child controls call Relay, which is then responsible for relaying the events up the control hierarchy. There are two types of events that must be hooked, those that occur when the controls are added or removed, and those that are to be relayed. The reason for monitoring when the controls are added or removed is to enable the dynamically added controls to respond to the relayed events just as their static counterparts do. The other type of event is the one provided when EventBroadcastProvider is initialized. Because the CreateProvider method takes the event name as a string, that event must be dynamically located using reflection and the event handler associated at that time. The following code demonstrates how the events are hooked and how recursion is used to hook all the controls in the control hierarchy. A similar method removes the handlers when controls are removed from the hierarchy:

private void HookPrimaryEvents(Control control)
{
    //

    // hook static events.

    //

    control.ControlAdded += m_controlAddedHandler;
    control.ControlRemoved += m_controlRemovedHandler;

    //

    // Use reflection to look for event that will be hooked.

    //

    Type typ = control.GetType();
    EventInfo theEvent = typ.GetEvent(m_eventName);
    theEvent.AddEventHandler(control, m_genericHandler);

        //

    // recursively hook events for nested controls

    //

    foreach (Control subControl in control.Controls)
    {
        HookPrimaryEvents(subControl);
    }
}

Relaying events

Relaying events ensures that when an event is raised on a control, all the ancestor controls also receive that event for handling. But not all ancestor controls need to handle the event.

The following code demonstrates the process of relaying events up the control hierarchy:

protected void Relay(object sender, EventArgs ea)
{
    //

    // Make sure to end when the target control is the 

    // only one that send the event. This is because 

    // it is assumed that there is already

    // that event being handled directly.

    //

    if (object.ReferenceEquals(sender, m_control))
        return;

    //

    // Must flag object to prevent reentry.

    //

    if (m_noReentry) return;

    //

    // Locate the event method used to raise the event. 

    // Currently must be in the format On<EventName>(...).

    //

    string eventMethName =
        String.Format(EventMethodTemplate, m_eventName);

    //

    // Invoke the event passing the event arguments for 

    // each ancestor

    //

    m_noReentry = true;
    Control curControl = ((Control)sender).Parent;
    while (curControl != null)
    {
        MethodInfo mi =
            curControl.GetType().GetMethod(eventMethName, 
                                  BindingFlags.NonPublic |
                                  BindingFlags.Public |
                                  BindingFlags.FlattenHierarchy |
                                  BindingFlags.Instance);

        //

        // Ignore any classes that do not support the event.

        //

        if (mi != null)
        {
            mi.Invoke(curControl, new object[] { ea });
        }

        //

        // End loop it target control has been reached.

        //

        if (object.ReferenceEquals(curControl, m_control))
            break;

        //

        // Get the parent, or null if no parent exists.

        //

        curControl = curControl.Parent;
    }
    m_noReentry = false;
}

Reentry can be a problem for a common method. This is because when the Click event, for instance, is invoked it is also invoked for all ancestor controls. Therefore, as the event is relayed to ancestor controls, they also invoke the Relay method as a response to the event; however, the purpose of Relay is to walk the control hierarchy which already happened when the first control's event was raised. To prevent this, a flag indicates that the method is currently being processed and therefore should not allow reentry. Once the handler is done processing the other controls, the flag is cleared.

The primary work is handled in a loop that evaluates each control's parent. The standard convention for invoking events is through the On� method such as OnClick(�). The convention is for the method to contain the name of the event; therefore this method can be easily derived from the targeted event name. Once found, the method can be invoked thereby raising the event for the parent control. This process continues until Relay has reached the control specified when creating the EventBroadcastProvider instance.

Creating dynamic event handler

During the implementation of EventBroadcastProvider, it was determined, as an EventHandler cannot be down-casted, that a strongly-typed delegate must be created. This process is handled automatically and is transparent to the consumer.

The following diagram shows how this is handled. The only class the consumer has to worry about is the EventBroadcastProvider; however, when CreateProvider is called, the class is dynamically created using reflection. Therefore, EventBroadcastProvider can ask the derived class for the strongly-typed delegate through the RelayDelegate property, which is then hooked to the event:

The following code is responsible for actually creating the derived class. Don't be intimidated by the amount of operations emitted. The only purpose of the derived class is to provide a strongly-typed event handler that, when called, will pass the responsibility back to the Relay method in EventBroadcastProvider (which is where the ancestor controls are visited).

[Editor comment: Line breaks used to avoid scrolling.]

private static EventBroadcastProvider CreateHandlerForEvent(
                                                  EventInfo ei)
{
    EventBroadcastProvider result = null;

    string namespaceName = 
        typeof(EventBroadcastProvider).Namespace;
    string eventName = ei.Name;
    string className = eventName + "BroadcastProvider";

    AssemblyName assemblyName = new AssemblyName();
    assemblyName.Name = className + "Assembly";

    AppDomain appDomain = AppDomain.CurrentDomain;

    AssemblyBuilder assBuilder = 
        appDomain.DefineDynamicAssembly(assemblyName, 
                                  AssemblyBuilderAccess.Run);
    ModuleBuilder modBuilder =
        assBuilder.DefineDynamicModule(className + "Module");
    TypeBuilder typBuilder =
        modBuilder.DefineType(className, TypeAttributes.Public, 
                                typeof(EventBroadcastProvider));

    FieldBuilder fldBuilder =
        typBuilder.DefineField("m_handler", typeof(Delegate), 
                                     FieldAttributes.Private);

    ILGenerator ilGen = null;

    //

    // Build the RelayDelegate property.

    //

    PropertyBuilder relayDelegateBuilder = 
        typBuilder.DefineProperty("RelayDelegate", 
                                  PropertyAttributes.None, 
                                  typeof(Delegate), null);
    MethodBuilder get_relayDelegateBuilder = 
        typBuilder.DefineMethod("get_RelayDelegate", 
                                MethodAttributes.HideBySig|
                                MethodAttributes.Virtual|
                                MethodAttributes.Family|
                                MethodAttributes.SpecialName, 
                                typeof(Delegate), null);

    ilGen = get_relayDelegateBuilder.GetILGenerator();
    ilGen.DeclareLocal(typeof(Delegate));
    ilGen.Emit(OpCodes.Nop);
    ilGen.Emit(OpCodes.Ldarg_0);
    ilGen.Emit(OpCodes.Ldfld, fldBuilder);
    ilGen.Emit(OpCodes.Stloc_0);
    ilGen.Emit(OpCodes.Ldloc_0);
    ilGen.Emit(OpCodes.Ret);

    relayDelegateBuilder.SetGetMethod(get_relayDelegateBuilder);

    //

    // Build the HandleEvent method.

    //

    MethodBuilder handleEventBuilder =
        typBuilder.DefineMethod("HandleEvent", 
        MethodAttributes.Private | MethodAttributes.HideBySig, 
        null, 
        new Type[] { typeof(object), 
            ei.EventHandlerType.GetMethod("Invoke").
                          GetParameters()[1].ParameterType });
    ilGen = handleEventBuilder.GetILGenerator();

    ilGen.Emit(OpCodes.Nop);
    ilGen.Emit(OpCodes.Ldarg_0);
    ilGen.Emit(OpCodes.Ldarg_1);
    ilGen.Emit(OpCodes.Ldarg_2);
    ilGen.Emit(OpCodes.Call, 
        typeof(EventBroadcastProvider).GetMethod("Relay", 
        BindingFlags.Instance|BindingFlags.NonPublic));
    ilGen.Emit(OpCodes.Nop);
    ilGen.Emit(OpCodes.Ret);

    //

    // Build the constructor.

    //

    ConstructorBuilder ctorBuilder =
        typBuilder.DefineConstructor(
                    MethodAttributes.Public| 
                    MethodAttributes.HideBySig, 
                    CallingConventions.HasThis, null);

    ilGen = ctorBuilder.GetILGenerator();
    ilGen.Emit(OpCodes.Ldarg_0);
    ilGen.Emit(OpCodes.Ldnull);
    ilGen.Emit(OpCodes.Stfld, fldBuilder);
    ilGen.Emit(OpCodes.Ldarg_0);
    ilGen.Emit(OpCodes.Call, 
        typeof(EventBroadcastProvider).
        GetConstructor(BindingFlags.Instance|
                       BindingFlags.NonPublic, null, 
                       Type.EmptyTypes, null));
    ilGen.Emit(OpCodes.Nop);
    ilGen.Emit(OpCodes.Nop);
    ilGen.Emit(OpCodes.Ldarg_0);
    ilGen.Emit(OpCodes.Ldarg_0);
    ilGen.Emit(OpCodes.Ldftn, handleEventBuilder);
    ilGen.Emit(OpCodes.Newobj, 
        ei.EventHandlerType.GetConstructors()[0]);
    ilGen.Emit(OpCodes.Stfld, fldBuilder);
    ilGen.Emit(OpCodes.Nop);
    ilGen.Emit(OpCodes.Ret);

    result = (EventBroadcastProvider)Activator.
                CreateInstance(typBuilder.CreateType());

    return result;
}

Event bubbling

Event bubbling is a companion concept of event broadcasting. Web application developers are more comfortable with bubbling events because web applications support this concept, but Windows applications do not.

With small modifications, the EventBroadcastProvider can be converted into an EventBubbleProvider. This is currently being developed and a companion article will be written to demonstrate how to bubble Windows application events.

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