Custom Event Programming in .NET

in #utopian-io7 years ago (edited)

What Will I Learn?

You will learn implementation, and consumption of custom events in Windows form applications.

Requirements

.Net Framework

Difficulty

Intermediate

Tutorial Contents

  • Defining My Events
  • Raising the Events
  • Subscribing to Events
  • Event Marshalling
  • Closing Review

Custom Event Programming in .NET

A tutorial on the definition, implementation, and consumption of custom events in Windows form applications. 

Any seasoned developer remembers the days when reacting to changes in the state of an object was done by setting flags and using loops to poll the state of that object. While effective, this tactic does require the developer to invest some time in setting this up. However, if we learn to leverage the power of custom events, we can greatly simplify the effort as well as increase our potential for reuse.

Recently I was called on to create an application that had to monitor and route messages between several TCP/IP based connections to remote devices. Yeah, multi-threaded, asynchronous communications! That’s sarcasm folks. The real challenge here was to ensure the application was easy to maintain. I needed to break the work down into small units that fulfilled specific functions.

It didn’t take long to realize I wanted a user control that contained the socket object and managed all communication between my application and a specific device. An instance of this "monitor" control would then be placed on a form for each device I wanted to monitor. The form would handle routing messages between devices as well as logging all message traffic. Additionally, I was going to need a couple modal dialog boxes to handle specific interactions with my devices.

This left only one final challenge, since the "monitor" controls would handle all aspects of communication with the remote devices, how to alert objects outside of the monitor control of changes. The solution, as you can likely guess based on the title of this article, was to use customized event delegates.

The monitor control would raise specific events that other controls could then subscribe to. If a message arrived, the main form could log it. A sub-form could check to see if it was the type of message it was waiting for, the control itself could simply display it on its form, etc. Now I only had to set it up.

Defining My Events

I knew I had 5 "events" I wanted to be able to monitor for (waiting for connection, got connection, lost connection, message sent, and message received). Additionally, I knew I could break the signatures for these events into two categories: changes in connection state and message traffic notification.

For changes in connection state, there were several pieces of information I wanted to pass along in the event. To keep things simple, I decided to define a class that contained all these values instead of passing them back as individual elements. I defined a custom event argument called ConnEventArgs.

ConnEventArgs is a simple class that inherits from the .NET Framework EventArgs class. It has a constructor and several properties that expose private values. This will make it easy for me to pass these values back to the event without having to declare all the individual properties.

Listing 1: The definition of ConnEventArgs, my custom event argument 

public class ConnEventArgs : EventArgs  
{  
    private int myPlatformID = 0;  
    private DateTime myOccurredTimestamp = DateTime.UtcNow;  
    private IPEndPoint myRemoteEndPoint = null;  
    public IPEndPoint RemoteEndPoint  
    {  
        get { return myRemoteEndPoint; }  
    }  
    public int PlatformID  
    {  
        get { return myPlatformID; }  
    }  
    public DateTime OccurredTimestamp  
    {  
        get { return myOccurredTimestamp; }  
    }  
    public ConnEventArgs(DateTime _Timestamp, int _Platform, IPEndPoint _RemoteEP)  
    {  
        this.myOccurredTimestamp = _Timestamp;  
        this.myPlatformID = _Platform;  
        this.myRemoteEndPoint = _RemoteEP;  
    }  
} 

For message traffic events, I’m not going to pass some descendent of EventArgs, but instead the message object I had already defined.So, parameters determined, next I had to define the signatures of my custom events. I defined 5 publically accessible event delegates that corresponded to the items I wanted to be able to react too. Each of them receives both a sender, as well an argument specific to the type of event.

// this delegate fires when the data is received from device  
public delegate void ucMessageReceived(object sender, MyMessage _MyMessage);  
// this delegate fires when data is sent to the device  
public delegate void ucMessageSent(object sender, MyMessage _MyMessage);  
// this delegate fires when we start waiting for a remote connection  
public delegate void ucConnWaiting(object sender, ConnEventArgs e);  
// this delegate fires when a remote connection is established   
public delegate void ucConnEstablished(object sender, ConnEventArgs e);  
// this delegate fires when connection is lost/cancelled  
public delegate void ucConnTerminated(object sender, ConnEventArgs e); 

Well, we’ve defined our events and the arguments they pass along, but unless we declare instances of our events, the definitions themselves won’t actually do anything. Next stop… the monitor control.

Raising the Events

As previously mentioned, my monitor control was going to handle all communication between the application and the remote device. As such, it is perfectly positioned to know when each of these events needs to be raised. So the next step is to create instances of my events and make sure they are raised when they need to be.We’ve defined our custom events, but now we have to declare instances of them. These really don’t look much different than any other object declaration. They are public, use the event keyword to specify that they are events, have the type of event, and have names associated with them.

Listing 2: Declaring instances of our custom events

public event ucMessageRecieved MessageReceived;  
public event ucMessageSent MessageSent;  
public event ucConnWaiting ConnWaiting;  
public event ucConnEstablished ConnEstablished;  
public event ucConnTerminated ConnTerminated; 

Of course, at this point, these are just placeholders and have no value; we’ll get to that in a bit. We now need a way for my monitor control to raise the events. The following listing shows the event invoker methods I created.

Listing 3: Methods my control will use to help us raise our custom events

protected virtual void OnMessageReceived(MyMessage _MyMessage)  
{  
    if (MessageReceived != null) MessageReceived(this, _MyMessage);  
}  
protected virtual void OnMessageSent(MyMessage _MyMessage)  
{  
    if (MessageSent != null) MessageSent(this, _MyMessage);  
}  
protected virtual void OnConnWaiting(ConnEventArgs e)  
{  
    if (ConnWaiting != null) ConnWaiting(this, e);  
}  
protected virtual void OnConnEstablished(ConnEventArgs e)  
{  
    if (ConnEstablished != null) ConnEstablished(this, e);  
}  
protected virtual void OnConnTerminated(ConEventArgs e)  
{  
    if (ConnTerminated != null) ConnTerminated(this, e);  
}  

These are protected methods, so they can only be used by my control or children that descend from it. And because these methods are also virtual, they can be overridden by any children. I could just as easily have made them private, but this way, things are a bit more flexible if we have to tweak this code down the road.Lastly, when appropriate, I need to make sure I raise my new events, using these invoker methods.

MyMessage tmp =   
  new MyMessage(myPlatformID, tRecievedTimestamp,  
    (IPEndPoint)socketData.m_currentSocket.RemoteEndPoint,   
    new System.String(chars).ToString());  
OnMessageReceived(tmp); // call my invoker method to raise the event 

This creates an instance of the MyMessage object, then calls the OnMessageReceived invoker to raise the event. It is important to note that the signature of the event and the invoker do not have to match. In fact, the use of the invoker is entirely optional. If you look at the invokers, they’re just checking to make sure the instances of the events I created have value and then calling those events with the appropriate parameters. You could easily do that yourself, but if you need to raise the event in multiple locations, using the invoker will save you typing later on.

Subscribing to Events

So we’ve defined our events and their parameters. We then declared the events in an object and given it the ability to raise them. At this point the application could run and we’d happily start raising events. Problem is that null check that the invoker is doing…. It would be null, so the events wouldn’t really be raised. We have to have controls subscribe to our new events so we can perform actions when they are raised. This requires two steps. First we define an event handler, and secondly we associate that handler with the events being raised by the monitor control.

Now the event handlers are really just methods. However, since they are specifically made to handle events, they need to match the signature of the event they are handling. We defined those signatures back in Figure 2. Now we just need to create a method that implements one of those. We singled out OnMessageReceived earlier, so we’ll stick with it.

Listing 4: Sample event handler for OnMessageReceived

private void MessageReceived(object sender, MyMessage _MyMessage)  
{  
    if (_MyMessage.ScrubbedPayload.Length > 0)  
    {  
          myTextLogger.WriteReceived(_MyMessage); // log message  
          ProcessMessage(_MyMessage);             // process it  
    }  
}  

Note that the MessageReceived method matches the parameters we defined in the declaration of ucMessageReceived. It also matches the parameters we used in the controls invoker method. Hopefully, the picture should be starting to come together now.Inside this event handler, we can do whatever we need to. In the case of my applications main form I’m going to make sure the message had a payload I want to act on, then log it and process (aka route) the message.Before this method can even be called, we need to subscribe to the event.

Listing 5: Wiring up event handlers

_Monitor.MessageRecieved += new ucMessageRecieved(MessageRecieved);  
_Monitor.MessageSent += new ucMessageSent(MessageSent);  
_Monitor.ConnWaiting += new ucConnWaiting(AwaitingConn);  
_Monitor.ConnEstablished += new ucConnEstablished(ConnEstablished);  
_Monitor.ConnTerminated += new ucConnTerminated(ConnTerminated); 

A handful of assignment statements and we are done. On the left-hand side, we have my monitor control and those public events we defined in the previous section. On the right-hand side, we define new instances of the event objects and are passing along to them the names of our event handler methods. This is sometimes referred to as a “callback reference” as we are passing along a reference to a method that will be called by another class at the appropriate time.

Note also that our assignment is +=, or add to what already exists. This is because other objects may also be subscribing to these events. So we want to keep any pre-existing subscriptions as we add our own.

Event Marshalling

One last topic I want to touch on briefly is how events are marshaled. When we subscribed to our events up above, this was likely done from within a form or another user control. And normally, under those circumstances you’re going to want to interact with other controls, status messages on labels, updating a displayed log of events, etc… However, depending on which thread the event marshaled into, you may encounter the dreaded “invalid cross thread operation” error. There are several ways to handle this, but my favorite is also the most straight-forward.By making a minor change to the code for our event handler, we can force the event to be marshaled into the thread responsible for all our other UI operations.

Listing 6: A thread safe version of our event handler

private void MessageReceived(object sender, DateTime _Received, string _Message)  
{  
    if (!InvokeRequired)  
    {  
       if (_MyMessage.ScrubbedPayload.Length > 0)  
       {  
           myTextLogger.WriteReceived(_MyMessage); // log message  
           ProcessMessage(_MyMessage);             // process it  
       }  
    }  
    else  
    {  
        Invoke(new ucMessageRecieved(MessageReceived), sender, _Received, _Message);  
    }  
} 

InvokeRequired is a property of a control (and therefore form) that allows you to detect if the event was called from another thread. If it was, we then use the control’s Invoke method to execute the event delegate on the same thread that owns the control. It’s a simple addition that can save you some headache later on.

Closing Review

Time for a quick summary of what we’ve done:

  1. Optionally define any custom event parameters we want to pass
  2. Define our custom event signatures
  3. Declare placeholders for the events in the class that will be raising the events
  4. Optionally define an invoker to help raise your event
  5. In the class that is raising the events, call the invokers when appropriate
  6. Create event handlers in classes that want to respond to our custom events
  7. Wire up or subscribe to the events.

Using this technique, you greatly expand the interaction between components in our windows applications. No longer are you confined by procedural based constructs like polling and status flags. Now you can alert other classes to changes in state proactively and let them react to it as they feel they should. Better yet, you can now enforce encapsulation and get rid of all those unnecessary global values you were using to communicate between components of your application. Explore, use, and enjoy this new and important addition to your developer’s toolkit. 



Posted on Utopian.io - Rewarding Open Source Contributors

Sort:  

Thank you for the contribution. It has been approved.

You can contact us on Discord.
[utopian-moderator]

Hey @dorodor I am @utopian-io. I have just upvoted you!

Achievements

  • You have less than 500 followers. Just gave you a gift to help you succeed!
  • Seems like you contribute quite often. AMAZING!

Suggestions

  • Contribute more often to get higher and higher rewards. I wish to see you often!
  • Work on your followers to increase the votes/rewards. I follow what humans do and my vote is mainly based on that. Good luck!

Get Noticed!

  • Did you know project owners can manually vote with their own voting power or by voting power delegated to their projects? Ask the project owner to review your contributions!

Community-Driven Witness!

I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!

mooncryption-utopian-witness-gif

Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x

Coin Marketplace

STEEM 0.19
TRX 0.15
JST 0.029
BTC 63643.10
ETH 2582.85
USDT 1.00
SBD 2.75