简体   繁体   中英

How to subscribe to events raised in multiple instances of a class?

I'm working on a small Unity project with C#.

I have a class UnitManager that contains a list of instances of class Unit . I want to raise an event, whenever a property (eg Health) changes within an instance of Unit . I am also using the UnitManager to store the eventHandlers. For example, I have a UI manager that subscribes to the event in the UnitManager .

The problem I am having is that I don't want the Unit class to know about the UnitManager class. The code I have right now (below) works, but I think this would be considered coupled code?

public class Unit
{
    private int health;
    public int Health {
        get { return health; }
        set
        {
            health = value;
            UnitManager.Instance.OnHealthChanged(this); //Unit shouldn't call UnitManager, right?
        }
    }
}

public class UnitManager
{

    protected List<Unit> units = new List<Unit>();

    public delegate void UnitHealthChangedEventHandler(object source, UnitEventArgs args);
    public event UnitHealthChangedEventHandler HealthChanged;

    public virtual void OnHealthChanged(Unit _unit)
    {
        if (HealthChanged != null)
        {
            HealthChanged(this, new UnitEventArgs() { unit = _unit });
        }
    }
}

public class UIManager
{
    void Start()
    {
        UnitManager.Instance.HealthChanged += OnUnitHealthChanged;
    }
}

According to an MSDN sample , the event handlers should be stored IN the Unit (the event sender). The difference is that the sample on MSDN has only one instance of "Counter", but my code has multiple instances. This would mean I have to loop through all instances of Unit and then subscribe to all of them. I'm afraid this would become a performance issue. (Especially since I'm having this same issue in multiple places in my code)

To summarize my question: What is the best way (in terms of OOP/loosely coupling) to let an EventHandler handle events that are raised in multiple instances of a class?

Try this:

public class HealthChangedEventArgs
{
    public HealthChangedEventArgs(int health) { Health = health; }
    public int Health  { get; private set; } // readonly
}

public class Unit
{
  //The delegate - event handlers must have this signature
    public delegate void HealthChangedEventHandler(object sender, HealthChangedEventArgs e);

    // The event
    public event HealthChangedEventHandler HealthChangedEvent;

    //Method for raising the event - derived classes can also call it.
    protected virtual void RaiseHealthChangedEvent()
    {
        // Raise the event by using the () operator.
        if (HealthChangedEvent != null)
            HealthChangedEvent(this, new HealthChangedEventArgs(health));
    }

    private int health;
    public int Health
    {
        get { return health; }
        set
        {
            health = value;
            RaiseHealthChangedEvent();
        }
    }
}

Unit isn't calling a method on UnitManager . It's just raising an event. It doesn't know what - if anything - is listening.

UnitManager is responsible for adding its event handler to each instance of Unit and listening for the event.

Are the listeners created at startup or could they be resolved by a dependency injection container?

Another approach which reduces coupling and consolidates attaching event handlers is using a domain event bus.

You have a singleton EventBus class - some use a static class, I prefer to use an IEventBus interface and inject the event bus into a class that may raise events.

Then you register classes as handlers for specific types of events. So you might have a HealthChangedEvent class, an IEventHandler<HealthChangedEvent> interface, and then you register classes that implement the interface.

That's the key - a class is registered as a handler for a type of event, so it doesn't have to subscribe to individual classes that might publish events. If anything raises a HealthChangedEvent then every registered handler receives it. You're registering a few listeners, not subscribing to lots of publishers.

When your class calls eventBus.Raise(healthChangedEvent) the event bus passes the event to every registered handler. That way you can have any number of listeners that are decoupled from the sender. They only know about the event. They don't know about the source unless a reference to the source is passed with the event.

It works especially well with dependency injection containers because the container can resolve instances of IEventHandler<TEvent> .

Here's a blog post on creating your own. I have my own implementation that allows me to use different DI containers without becoming coupled to any. I'll put that into a blog post so that it's easier to share.


Update: Here's my own implementation . I'll put it in a repository at some point along with all the unit tests.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM