user164226
user164226

Reputation:

Help me write this event The Right Way

Original Question

I've read at least a dozen articles and SO threads on events and have puzzled out the basic ideas, but I'm a bit muddy on how to do it The Right Way™. It seems that there are at least two common approaches to writing events, and one is more recommended than the other.

I'm seeing a lot of material in which the author skips parts of the process, assuming for some reason that you would just know that. There's also a lot of tutorialese like "MyEventExample" and "SomeProcessGoesHere" which makes it harder to grok the example as a whole. Many examples go through all the trouble of teaching you how to do something, only to state at the end of it that you'd of course never do that in reality - but then they don't provide the way you would do it.

Finally, the naming conventions for each of the components of a scenario using events seem to be all over the map. I have a lot of trouble figuring out where certain concepts are being applied because everyone names every part of it differently.

So here's what I'm after: I have a simple situation in a game which would make use of events. I'd like someone to come in and write the event wireup, demonstrating the recommended approach and the most common naming and structural conventions. I know it's poor form to ask for code to be written for me, but I'm really looking for the way to write it so I can start confidently doing it for myself.

Please ignore whether this is a good game design, or even whether it's an appropriate situation for events. I'm just interested how to properly write the event stuff, and this is my example space.


//In my game I have a number of entities which can 'talk' to the player. 
//An NPC can greet them, a zone might 'greet' them by displaying "Cityville" 
//when they enter it, their inventory might tell them they're out of space, 
//and so on. Everything would pass a Message object to a Messenger object, 
//which would then deliver the messages as it saw fit.

public class Messenger
{
    private Queue<Message> _messages = new Queue<Message>();;
    private Stack<Message> _archive = new Stack<Message>();;

    public IEnumerable<Message> Messages { get { return _messages.AsEnumerable(); } }
    public IEnumerable<Message> Archive { get { return _archive.AsEnumerable(); } }

    public void Add(Message message)
    {
        _messages.Enqueue(message);
    }
    public void Deliver()
    {
        Message msg = _messages.Dequeue();
        _archive.Push(msg);

        //Here's where I'd broadcast to any subsystem listening 
        //that the message was delivered 
        //Event args should be (_archive.Peek(), DateTime.Now);
            
    }
        
    public event MessageDeliveryEvent Delivery;
    protected virtual void OnDelivery(MessageHandlerDeliveryEventArgs e)
    {
        if (this.Delivery != null) { this.Delivery(this, e); }
    }
}

//Okay, here's my delegate declared outside any class. One tutorial suggested 
//putting it in the same file as the event arguments class so it would be 
//easier to find, which sounds reasonable to me, but I dunno.

public delegate void MessageDeliveryEvent(object sender, MessageHandlerDeliveryEventArgs e);

//I've seen examples where they don't even write an event arguments class. 
//I think you could probably just pass the same stuff directly, but the 
//separate class sounds like a good idea, more flexible if things change.

public class MessageHandlerDeliveryEventArgs : EventArgs
{
    private readonly Message _message;
    private readonly DateTime _delivered;

    public MessageHandlerDeliveryEventArgs(Message message, DateTime delivered)
    {
        _message = message;
        _delivered = delivered;
    }

    public Message Message { get { return _message; } }
    public DateTime DeliveryDateTime { get { return _delivered; } }
}

//So in the UI layer I'd have things like a ChatBox which would be a
//scrolling list of everything said to the player. There would also be a 
//GeneralInfoBox control that displayed things like what zone you just 
//entered, or that your inventory is full. Both would listen to the 
//Messenger object for a delivery event, and then check the Message object 
//associated with that event to see if they needed to handle the display 
//of that message.

public class ChatBox
{
    //OMG there's a new message, lemme see if I should display it
    private void TheThingThatListensToMessengerEvents(Message message, DateTime datetime)
    {
        if Message.Type == MessageType.Chat { Print(datetime.ToString() + ": " + message.Text); }
    }
    public string Print(string text) {}
}
public class GeneralInfoBox
{
    //OMG there's a new message, lemme see if I should display it
    private void TheThingThatListensToMessengerEvents(Message message)
    {
        if Message.Type == MessageType.General { Print(message.Text); }
    }
    public string Print(string text) {}
}

If I can clarify anything, let me know. If there's a really good tutorial I've obviously missed, please feel free to just point me to that. Thanks in advance.


What I've taken from the thread

So here's my example with events wired into it. Maybe it will help someone else who thinks like me (God help them) visualize it.


public class MessageHandler
{
    private Queue<Message> _messages = new Queue<Message>();
    private Stack<Message> _archive = new Stack<Message>();

    public MessageHandler() { }

    public IEnumerable<Message> Messages { get { return _messages.AsEnumerable(); } }
    public IEnumerable<Message> Archive { get { return _archive.AsEnumerable(); } }

    public void Add(Message message)
    {
        _messages.Enqueue(message);
    }
    public void Deliver()
    {
        Message msg = _messages.Dequeue();
        _archive.Push(msg);

        //Call the method which broadcasts the event
        OnDelivery(new MessageDeliveryEventArgs(_archive.Peek(), DateTime.Now));
    }

    //The event
    public event EventHandler<MessageDeliveryEventArgs> Delivery;

    //The method which broadcasts the event
    protected virtual void OnDelivery(MessageDeliveryEventArgs messageDeliveryEventArgs)
    {
        EventHandler<MessageDeliveryEventArgs> handler = Delivery;
        if (handler != null) { handler(this, messageDeliveryEventArgs); }
    }
}

//The event arguments class for the event of interest. Carries information about this kind of event
public class MessageDeliveryEventArgs : EventArgs
{
    private readonly Message _message;
    private readonly DateTime _delivered;

    public MessageDeliveryEventArgs(Message message, DateTime delivered)
    {
        _message = message;
        _delivered = delivered;
    }

    public Message Message { get { return _message; } }
    public DateTime DeliveryDateTime { get { return _delivered; } }
}

//A UI control which listens for an event in a Messenger object
public class ChatBox
{
    //Specify the instance of the Messenger class to whose event(s) we plan to subscribe
    public ChatBox(MessageHandler messenger)
    {
        //Subscribe this control's OnDelivery method to the Delivery event of the specified instance of Messenger
        messenger.Delivery += this.OnDelivery;
    }

    //The method which we intend to subscribe to the Delivery event of an instance of Messenger
    private void OnDelivery(object sender, MessageDeliveryEventArgs e)
    {
        if (e.Message.Format == MessageFormat.Local)
        {
            Print(String.Format("{0}: {1}", e.DeliveryDateTime, e.Message.Text));
        }
    }
    private void Print(string text) { }
}

Upvotes: 4

Views: 213

Answers (2)

Derek Greer
Derek Greer

Reputation: 16262

Here is an example of a typical convention followed for setting up standard .Net events:

using System;

namespace ObserverExample
{
    class Program
    {
        static void Main(string[] args)
        {
            var subject = new Subject();
            var observer = new Observer();
            observer.Observe(subject);
            subject.SomeAction();
            Console.ReadLine();
        }
    }  

    public class Subject
    {
        public event EventHandler<TopicEventArgs> TopicHappening;

        public void SomeAction()
        {
            OnTopicHappening(new TopicEventArgs("Hello, observers!"));
        }

        protected virtual void OnTopicHappening(TopicEventArgs topicEventArgs)
        {
            EventHandler<TopicEventArgs> handler = TopicHappening;

            if (handler != null)
                handler(this, topicEventArgs);
        }
    }

    public class TopicEventArgs : EventArgs
    {
        public TopicEventArgs(string message)
        {
            Message = message;
        }

        public string Message { get; private set; }
    }

    public class Observer
    {
        public void Observe(Subject subject)
        {
            subject.TopicHappening += subject_TopicHappening;
        }

        void subject_TopicHappening(object sender, TopicEventArgs e)
        {
            Console.WriteLine(e.Message);
        }
    }
}


The three primary classes involved in this example are the Subject, Observer, and the TopicEventArgs. The Program class serves only to provide a driver for the example.

Looking first at the Program.Main() method, instances of Subject (the object that will be raising events) and Observer (the object that will be subscribing to the raised events) are first instantiated. Next, the observer is passed an instance of the subject allowing it the opportunity to subscribe to any desired events. Lastly, the subject's SomeAction() method is called which results in the raising of the event.

Looking at the Subject, we see that an event named TopicHappening of type EventHandler<TopicEventArgs> is publicly declared. The EventHandler type was introduced in .Net 2.0 and allows events to be declared without having to explicitly define delegate types. The Subject class also has two methods, SomeAction() and OnTopicHappening(). The method SomeAction() represents the point within the application where the Subject performs some task for which it wants to notify the world (i.e. "any observers") about. The method OnTopicHappening(TopicEventArgs) method provides the logical point within the class where the event will be raised. First, notice that it follows the naming convention On[Name of the event]. While this method can be named anything, this pattern has been widely adopted by convention. Second, notice that it is defined to take a single argument of type TopicEventArgs. This also follows a standard convention and serves the purpose of keeping the decision of what event arguments are raise at the logical point the event is raised (within the SomeAction() method) rather than from the physical point where the event is raised. Third, notice that it is declared protected virtual. This pattern is generally followed to allow any classes extending Subject to override what exactly happens when the TopicHappening event is raised. Within the OnTopicHappening() method, the TopicHappening event is assigned to a separate variable before the event is checked for null and invoked. This is to avoid a possible race condition where the event may be cleared by another thread (i.e. all observers unsubscribed) after the check for null, but before the event is invoked.

Looking at the TopicEventArgs class, this represents the event topic our subject raises. A custom EventArgs class is generally created when the subject needs to associate information with the event being raised. For subjects which only wish to send a signal event without any associated arguments, the base EventArgs.Empty class should be used.

Lastly, the Observer class defines the objects which will receive the event notifications from the Subject. In this example, the Observer class exposes an Observe() method just as a way to receive a reference to an instance of Subject. Within the method, a private event handler method named subject_TopicHappening is assigned to the TopicHappening event on the subject. This name format is the result of the delegate automatically generated by Visual Studio upon typing += when registering to handle the event. This essentially adds this method to the collection of methods to invoke when the event is raised by the subject. When invoked, the private subject_TopicHappening method simply writes the message contained within the event arguments out to the Console.

Hope this helps.

Upvotes: 3

Bryan Watts
Bryan Watts

Reputation: 45445

An event is basically a list of methods. To add to that list, first create a method which has the matching signature, then use += on the event field of the object which declares the event:

public class ChatBox
{
    public ChatBox(Messenger messenger)
    {
        messenger.Delivery += OnMessageDelivery;
    }

    private void OnMessageDelivery(object sender, MessageHandlerDeliveryEventArgs e)
    {
        if(e.Message.Type == MessageType.Chat)
        {
            Print(String.Format("{0}: {1}", e.DateTime, e.Message.Text));
        }
    }
}

The += wraps the method in a delegate and appends it to the existing list of delegates represented by Delivery. The handler is now associated with the event on that particular instance of Messenger.

Also, you don't need to use a custom delegate type. You can declare the event like this:

public event EventHandler<MessageHandlerDeliveryEventArgs> Delivery;

When someone is browing this style of API, they don't have to use Go To Definition to see what kind of EventArgs come with the event. You also have one less type to maintain and don't have to answer the question "does the delegate go in this file or a separate one?" (My answer is that it goes in a separate one; it is a type like any other and deserves its own artifact, even though you can write it in one line.)

Upvotes: 2

Related Questions