Event Handlers and Interfaces

34,347

Solution 1

No, your handlers will not work because they're attached to the old object. Interfaces provides...an interface to an object, see it as a kind of contract but they're not a different object themselves.

If you need to switch between different implementations of the interface (at run-time) and to keep all handlers working you have to have the same object reference for the interface itself, kind of strategy pattern (more or less).

In your case you may, for example, implement the IDataIO interface in a DataIO object. It'll expose a property (or a method, I think its intent is more clear) to switch between different implementations of that interface (serial, TCP or whatever). It'll be the only one object to attach an event handler to that interface (and it'll drop the handler when the concrete implementation will change). Users of that object will always see it, whatever it's the concrete implementation it's using.

Example

This is a small example to explain this concept. The generic interface is this:

interface IDataIO
{
    void Write(byte[] data);

    byte[] Read();

    event EventHandler DataReceived;
}

This is the concrete implementation of IDataIO, other classes will use only this class directly:

sealed class DataIO : IDataIO
{
    public void SetChannel(IDataIO concreteChannel)
    {
        if (_concreteChannel != null)
            _concreteChannel.DataReceived -= OnDataReceived;

        _concreteChannel = concreteChannel;
        _concreteChannel.DataReceived += OnDataReceived;
    }

    public void Write(byte[] data)
    {
        _concreteChannel.Write(data);
    }

    public byte[] Read()
    {
        return _concreteChannel.Read();
    }

    public event EventHandler DataReceived;

    private IDataIO _concreteChannel;

    private void OnDataReceived(object sender, EventArgs e)
    {
        EventHandler dataReceived = DataReceived;
        if (dataReceived != null)
            dataReceived(this, e);
    }
}

Finally some code for testing:

class Test
{
    public Test()
    {
        _channel = new TcpIO();

        _channel.DataReceived += OnDataReceived;
    }

    public void SetChannel(IDataIO channel)
    {
        _channel.SetChannel(channel);

        // Nothing will change for this "user" of DataIO
        // but now the channel used for transport will be
        // the one defined here
    }

    private void OnDataReceived(object sender, EventArgs e)
    {
        // You can use this
        byte[] data = ((IDataIO)sender).Read();

        // Or this, the sender is always the concrete
        // implementation that abstracts the strategy in use
        data = _channel.Read();
    }

    private DataIO _channel;
}

Solution 2

Obviously you should consider the strategy pattern. I will post the code first and explain later:

public interface IDataIO
{
    event DataReceivedEvent DataReceived;

    //this the new added method that each IO type should implement.
    void SetStrategy();
}

public class SerialIO : IDataIO
{
    public void SetStrategy()
    {
        //put the code that related to the Serial IO.
        this.DataReceivedHandler += MyGuiUpdate;
        this.DataReceivedHandler += MyDataProcessing;
    }
}

public class TcpIO : IDataIO
{
    public void SetStrategy()
    {
        //put the code that related to the Tcp IO.
        //I will not implement it because it is a demo.
    }
}

public class UdpIO : IDataIO
{
    public void SetStrategy()
    {
        //put the code that related to the Udp IO.
        //I will not implement it because it is a demo.
    }
}

public class IO
{
    IDataIO port = new IDataIO();

    public void SetIOType(IDataIO ioType)
    {
        this.port = ioType;

        port.SetStrategy();
    }

}

public class IOManager
{
    List<IO> ports = new List<IO>();

    SerialIO serial = new SerialIO();
    TcpIO tcp = new TcpIO();

    ports[0].SetIOType(serial);
    ports[1].SetIOType(tcp);
}
  1. The interface IDataIO define basics that the all the IO types should implement.

  2. The SerialIO, TcpIO, UdpIO classes derived from IDataIO implement the method SetStrategy() to meet each of their own need.

  3. The IO class owns a field(named port) refers to a IDataIO type, this field can be setted to a certain IO type during the runtime by calling the method SetIOType() defined in the IO class. Once this method is being called, we know which type the 'port' field refers to, and then call the SetStrategy() method, it will run the overrided method in one of the IO class.

  4. The IOManager class is the client. when it needs a certain IO type, say SerialIO, it only need to new a IO class and call the SetIOType() method by passing a SerialIO class instance, and all the logic related to the SerialIO type will be setted automatically.

Hope my description can help you.

Share:
34,347
Simon
Author by

Simon

Technical Coordinator at Fugro TSM, Perth Australia B. Eng Software Engineering Delphi / C# / SQL Server / MySQL Experienced in subsea hardware communications and system development, database admin and creation, experienced radiographer and digital video application development and solutions.

Updated on July 09, 2022

Comments

  • Simon
    Simon almost 2 years

    I have an interface called IDataIO:

    public interface IDataIO
    {
      event DataReceivedEvent DataReceived;
      //.....more events,methods and properties
    }
    

    I also have multiple classes that implement this interface, namely UdpIO, TcpIO, SerialIO.

    Now, I have an IO class that allows me to switch between different input/output hardware. Each instance of this class has a CurrentIODevice property, which could be one of SerialIO,UdpIO or TcpIO. When this property is assigned, i attach 1 or more handlers to the DataReceivedEvent so that my GUI is notified when incoming data is received, as well as other classes that need to be notified.

    public class IO
    {
      IDataIO CurrentIODevice;
    
      public IO()
      {
        SerialIO serial = new SerialIO();
        TcpIO tcp = new TcpIO();
        UdpIO udp = new UdpIO();
        CurrentIODevice = serial;
      }
    }
    

    I also have a IOManager class that holds multiple IO objects.

    public class IOManager
    {
      List<IO> Ports = new List<IO>();
      public IOManager()
      {
        Ports.Add(new IO());
        Ports.Add(new IO());
      }
    
      Ports[0].CurrentIODevice = serial;
      Ports[0].CurrentIODevice.DataReceivedHandler += MyGuiUpdate;
      Ports[0].CurrentIODevice.DataReceivedHandler += MyDataProcessing;
    }
    

    My concern (its not an issue atm) is how I am going to change between different IDataIO interfaces at runtime.

    What is the effect of, at runtime, performing the following statement:

    //i know this is illegal but just to demonstrate
    IOManager.Ports[0].CurrentIODevice = tcp; 
    

    Will the event handlers still be functioning (and correctly)?

    Do i need to unassign the events before the CurrentIODevice is assigned, and then re-assign the handlers again after? If this is the case, I can see this approach getting quite messy, so if anyone has a better approach to this problem I'm all ears :)

  • Simon
    Simon almost 12 years
    +1 thanks, my original CurrentIODevice indeed unassigned and reassigned the event handlers in the setter method, but I agree a method probably makes more sense. If, when changing the interface object, I store a reference to all event handlers, unassign them, then reassign the stored event handlers to the new object - im assuming this would work?
  • Adriano Repetti
    Adriano Repetti almost 12 years
    @Simon it sounds error prone so it's not necessary. ;) DataIO implements IDataIO; users will attach to the event exposed by DataIO; only DataIO attaches to the event exposed by concrete implementation; when DataIO handles an event from the implementation it simply forward the event to its listeners (it raises its event). When you change strategy DataIO will detach its event handler from old implementation, will attach to the new one and nothing else.
  • Adriano Repetti
    Adriano Repetti almost 12 years
    @Simon Updated the answer with a small example