Besseres Event-Handling

    • C#
    • .NET 5–6

    Es gibt 2 Antworten in diesem Thema. Der letzte Beitrag () ist von φConst.

      Besseres Event-Handling

      -1-

      Der Titel mag ein wenig voreingenommen klingen, ist er auch, dennoch soll nachfolgend eine alternative Art des Event-Handlings gezeigt werden, die ganz ohne "event" keywords oder Delegates auskommt.

      Warum überhaupt alternatives Event-Handling?
      Weil die Art und Weise wie es aktuell gehandhabt wird (events, delegates und +=) insofern obsolet ist, dass diese sich nur sehr mühselig a) warten, b) verändern und c) isolieren lassen.
      Ziel ist einer losere Kopplung, sodass im Idealfall Änderungen in einer Klasse (etwa Event-Sender) nicht auf Änderungen in vielen anderen Klassen auswirkt. Ferner soll das Event-Handling im Allgemeinen viel flexibler möglich sein, als es momentan der Fall ist.

      Wie?
      Ganz simpel: Dependency-Injection. Eine event-werfende Klasse muss eine Referenz auf ein bestimmtes Interface halten. Es ist nun die Aufgabe einer übergeordnete Manager-Klasse, diese Referenz zu setzen.
      Möchte die Klasse nun ein Event werfen, muss diese lediglich die im Interface hinterlegte Methode (.Notify) aufrufen, und alle Klassen die Event-Empfänger sein wollen, werden benachrichtigt: Sender und Empfänger sind jeweils von einander entkoppelt.


      Source
      Spoiler anzeigen

      C#-Quellcode

      1. public sealed class NSEM
      2. {
      3. private static NSEM instance;
      4. public static NSEM Default()
      5. {
      6. if (instance == null)
      7. instance = new NSEM();
      8. return instance;
      9. }
      10. private Dictionary<Type, List<object>> typeInstances;
      11. private NSEM()
      12. {
      13. this.typeInstances = new Dictionary<Type, List<object>>();
      14. }
      15. public void Subscribe(object instance)
      16. {
      17. foreach (var irfc in Helper.GetNotificationReceivers(instance))
      18. {
      19. if (!typeInstances.ContainsKey(irfc))
      20. typeInstances.Add(irfc, new List<object>());
      21. typeInstances[irfc].Add(instance);
      22. }
      23. }
      24. public void Unsubscribe(object instance)
      25. {
      26. this.typeInstances[instance.GetType()].Remove(instance);
      27. }
      28. public IEnumerable<object> GetRelevantInstances<S>()
      29. {
      30. var instances = typeInstances[typeof(S)];
      31. for (int i = 0; i < instances.Count; i++)
      32. yield return instances[i];
      33. }
      34. public dynamic New<T>(params dynamic[]? arguments)
      35. {
      36. object[] injectedArguments = new object[arguments.Length + 1];
      37. injectedArguments[0] = new BasicNotificationAdapter<T>();
      38. Array.Copy(arguments, 0, injectedArguments, 1, arguments.Length);
      39. return Activator.CreateInstance(typeof(T), injectedArguments);
      40. }
      41. }

      C#-Quellcode

      1. public static class Helper
      2. {
      3. private static string INTERFACE_NAME = typeof(INotificationReceiver<dynamic>).Name;
      4. public static IEnumerable<Type> GetNotificationReceivers(object instance)
      5. {
      6. Type type = instance.GetType();
      7. foreach (var irfc in type.GetInterfaces())
      8. {
      9. string name = irfc.Name;
      10. if (name.Equals(INTERFACE_NAME))
      11. yield return irfc;
      12. }
      13. }
      14. }



      Das ist die so eben genannte Manager-Klasse. Soll eine neue event-werfende Klasse instanziiert werden, geschieht das über die Methode .New<T>().

      Die notwendigen Interfaces sehen so aus:

      Spoiler anzeigen

      C#-Quellcode

      1. public interface INotificationReceiver<T>
      2. {
      3. void Notify(object sender, T argument);
      4. }

      C#-Quellcode

      1. public interface INotificationAdapter
      2. {
      3. void Notify<S>(object sender, dynamic argument);
      4. }



      Ein möglicher (zu "injizierender") INotificationAdapter kann etwa so aussehen:

      Spoiler anzeigen

      C#-Quellcode

      1. public class BasicNotificationAdapter<P> : INotificationAdapter
      2. {
      3. public void Notify<S>(object sender, dynamic argument)
      4. {
      5. foreach (dynamic instance in NSEM.Default().GetRelevantInstances<S>())
      6. instance.Notify(sender, argument);
      7. }
      8. }



      Events lassen sich jetzt sehr leicht realisieren.

      Beispiel Sender-Klasse:
      Spoiler anzeigen

      C#-Quellcode

      1. class TestSender
      2. {
      3. INotificationAdapter adapter;
      4. public TestSender(INotificationAdapter adapter)
      5. {
      6. this.adapter = adapter;
      7. }
      8. public void Press()
      9. {
      10. this.adapter.Notify<INotificationReceiver<Guid>>(this, Guid.NewGuid());
      11. this.adapter.Notify<INotificationReceiver<int>>(this, 191);
      12. }
      13. }


      Der Sender muss lediglich eine Referenz auf INotificationAdapter halten, über den Konstruktor wird diese dann von der Manager-Klasse instanziiert.
      Möchte der Sender nun ein Event auslösen, so muss dieser jetzt .Notify() aufrufen und dann lediglich die Event-Meldung spezifizieren.

      Beispiel Empfänger-Klassen:
      Spoiler anzeigen

      C#-Quellcode

      1. public class TestSubscriberGuidListener : INotificationReceiver<Guid>
      2. {
      3. public TestSubscriberGuidListener()
      4. {
      5. NSEM.Default().Subscribe(this);
      6. }
      7. public void Notify(object sender, Guid argument)
      8. {
      9. Console.WriteLine("Notified with " + argument + " by " + sender);
      10. }
      11. }
      12. public class TestSubscriberIntListener : INotificationReceiver<int>
      13. {
      14. public TestSubscriberIntListener()
      15. {
      16. NSEM.Default().Subscribe(this);
      17. }
      18. public void Notify(object sender, int argument)
      19. {
      20. Console.WriteLine("Notified with " + argument + " by " + sender);
      21. }
      22. }


      Im Konstruktor muss jetzt nur noch angegeben werden, dass man auf Events lauschen möchte, ferner die Interface implementiert und die auf die zu lauschende Meldung spezifiziert werden.

      Die Ausführung sieht dann so aus:

      C#-Quellcode

      1. class Program
      2. {
      3. static void Main(string[] args)
      4. {
      5. TestSender sender = NSEM.Default().New<TestSender>();
      6. TestSubscriberGuidListener guidListener = new TestSubscriberGuidListener();
      7. TestSubscriberIntListener intListener = new TestSubscriberIntListener();
      8. sender.Press();
      9. Console.Read();
      10. }
      11. }


      Die Empfänger-Klassen kennen nicht den Sender, der Sender kennt nicht die Empfänger. Änderungen auf einen, oder gar beiden Seiten rufen keine tiefergreifenden Nachwirkungen hervor.

      Ergebnis:


      Jener Empfänger, der auf Guid-artige Meldungen lauscht, empfängt die Guid-Meldung, analog empfängt jener anderer der auf int-artige Meldungen wartet, die int-Meldung.
      _
      Und Gott alleine weiß alles am allerbesten und besser.

      Dieser Beitrag wurde bereits 4 mal editiert, zuletzt von „φConst“ ()

      Ich bin wohl derzeit geistig nicht in der Lage, den Nutzen nachzuvollziehen. Kannst Du das mal anhand eines … gängigen Beispiels (Button_Click?) ummünzen? Also CustomButtonKlasse sendet Click und FormKlasse empfängt.
      btw: Wofür steht NSEM?
      Wofür steht irfc in foreach (var irfc in Helper.GetNotificationReceivers(instance))
      Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

      Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.
      Kannst Du das mal anhand eines … gängigen Beispiels (Button_Click?) ummünzen? Also CustomButtonKlasse sendet Click und FormKlasse empfängt.

      Siehe NSEventDemo.zip


      Wofür steht irfc

      Interface
      Und Gott alleine weiß alles am allerbesten und besser.