Generyczny event

0

Piszę sobie statyczną klasę, która będzie mogła dostarczać wiele różnych typów zdarzeń. Powiedzmy, że wszystkie implementują jakiś interfejs IEvent. Mógłbym stworzyć jedno zdarzenie, które będzie wywoływane dla wszystkich zdarzeń (oraz powiedzmy, że dostarczę obok samego zdarzenia (typu IEvent) również Type albo jakiś enum i użytkownik sobie zrobi switcha czy drabinkę ifów. Ale nie podoba mi się to rozwiązanie, tak samo jak udostępnienie tylu zdarzeń, co moich typów. Ważne jest również to, że nie wszystkie będą one wykorzystywane oraz w mojej bazowej klasie chciałbym się w łatwy sposób dowiedzieć jakie typy zdarzeń zażyczył sobie użytkownik (w przypadku poprzedniego pomysłu i tak musiałbym dostarczyć jakąś dodatkową metodą te typy/enumy).

Najlepsze byłoby takie, które jest statycznie typowane (safe-type), a wywołanie zdarzenia nie jest późno wiązane (late-bound) i API było proste dla użytkownika. Pierwsze to kwestia wygody oraz bezpieczeństwa, a drugie wydajności, więc przekazywanie MulticastDelegate odpada (no chyba, że będziemy się bawić w magiczne sztuczki typu dynamiczne generowanie IL).

Niestety w .NET nie ma czegoś takiego jak generyczne zdarzenia (cholera no, przydały by się szablony ;)), ale można stworzyć delegate, który ma generyczny argument. Oczywiście taki delegate musiałby się znajdować w klasie, która ten parametr dostarczy. No i powstało mi takie coś:

using System;
using System.Collections.Generic;
using System.Linq;

namespace Rev.GenericEventsTest
{
    public interface IEvent { }

    public struct SomeEvent : IEvent { }
    public struct AnotherEvent : IEvent { }

    public static class Something
    {
        private static Dictionary<int, Tuple<Type, Action<IEvent>>> _events = new Dictionary<int, Tuple<Type, Action<IEvent>>>();

        private static void AddEvent(int identifier, Type type, Action<IEvent> invoker)
        {
            if(!_events.ContainsKey(identifier))
                _events.Add(identifier, new Tuple<Type, Action<IEvent>>(type, invoker));
        }

        private static void RemoveEvent(int identifier)
        {
            if(_events.ContainsKey(identifier))
                _events.Remove(identifier);
        }

        public static void InvokeAll(Type type, IEvent argument)
        {
            foreach (var @event in _events.Where(e => e.Value.Item1 == type))
                @event.Value.Item2(argument);
        }

        public static class Subscriber<T> where T : IEvent
        {
            private static int GenerateIdentifier(SomeEventHandler eventHandler)
            {
                return eventHandler.GetHashCode() | eventHandler.Method.GetHashCode();
            }

            public delegate void SomeEventHandler(T @event);

            public static event SomeEventHandler OnEvent
            {
                add
                {
                    AddEvent(GenerateIdentifier(value), typeof(T), GetInvoker(value));
                }

                remove
                {
                    RemoveEvent(GenerateIdentifier(value));
                }
            }

            private static Action<IEvent> GetInvoker(SomeEventHandler eventHandler)
            {
                return (IEvent @event) => eventHandler.Invoke((T)@event);
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Something.Subscriber<SomeEvent>.OnEvent += Program_OnEvent1;
            Something.Subscriber<SomeEvent>.OnEvent += Program_OnEvent1;
            Something.Subscriber<AnotherEvent>.OnEvent += Program_OnEvent1;
            Something.Subscriber<SomeEvent>.OnEvent += Program_OnEvent2;

            Something.InvokeAll(typeof(SomeEvent), new SomeEvent());

            Something.Subscriber<SomeEvent>.OnEvent -= Program_OnEvent1;

            Console.WriteLine();

            Something.InvokeAll(typeof(SomeEvent), new SomeEvent());

            Console.WriteLine();

            Something.InvokeAll(typeof(AnotherEvent), new AnotherEvent());
        }

        static void Program_OnEvent1(AnotherEvent ev)
        {
            Console.WriteLine("Program_OnEvent1 - AnotherType");
        }

        static void Program_OnEvent1(SomeEvent ev)
        {
            Console.WriteLine("Program_OnEvent1 - SomeEvent");
        }

        static void Program_OnEvent2(SomeEvent ev)
        {
            Console.WriteLine("Program_OnEvent2 - SomeEvent");
        }

    }
}

Z perspektywy użytkownika dołączenie swojego handlera jest bajecznie proste, usunięcie też. "Pod maską" będę sobie wywoływać coś w stylu InvokeAll (tutaj publiczny, żebym mógł rozwiązanie przetestować).

Czy moje rozwiązanie jest optymalne? Co można w nim ulepszyć, a może jest w ogóle jakiś inny sposób na rozwiązanie tego problemu? Bez upubliczniania metody typu AddEvent ;).

2

IMO trochę przekombinowałeś :> (a nawet bardziej niż trochę).
Moja wersja, na ile rozumiem problem - zupełnie typesafe i minimum boilerplate code:

public interface IEvent { }

public struct SomeEvent : IEvent { }
public struct AnotherEvent : IEvent { }

class EventArgs<T> : EventArgs
{
    public EventArgs(T data)
    {
        this.Data = data;
    }

    public T Data { get; private set; }
}

static class Something<T>
{
    public static event EventHandler<EventArgs<T>> OnEvent;

    public static void Invoke(T @event)
    {
        if (OnEvent != null) { OnEvent(null, new EventArgs<T>(@event)); }
    }
}

class Program
{
    static void Main(string[] args)
    {
        Something<SomeEvent>.OnEvent += Program_OnEvent1;
        Something<SomeEvent>.OnEvent += Program_OnEvent1;
        Something<AnotherEvent>.OnEvent += Program_OnEvent1;
        Something<SomeEvent>.OnEvent += Program_OnEvent2;
        Something<SomeEvent>.Invoke(new SomeEvent());

        Something<SomeEvent>.OnEvent -= Program_OnEvent1;
        Console.WriteLine();

        Something<SomeEvent>.Invoke(new SomeEvent());
        Console.WriteLine();

        Something<AnotherEvent>.Invoke(new AnotherEvent());
        Console.ReadKey();
    }

    static void Program_OnEvent1(object sender, EventArgs<AnotherEvent> ev)
    { Console.WriteLine("Program_OnEvent1 - AnotherType"); }

    static void Program_OnEvent1(object sender, EventArgs<SomeEvent> ev)
    { Console.WriteLine("Program_OnEvent1 - SomeEvent"); }

    static void Program_OnEvent2(object sender, EventArgs<SomeEvent> ev)
    { Console.WriteLine("Program_OnEvent2 - SomeEvent"); }
}

Trochę zmieniłem interfejs, w szczególności teraz jest Something<T> zamiast Something.Subscriber<T> (łatwo zmienić jeśli wolisz poprzednią wersję).
Druga zmiana -

static void Program_OnEvent1(SomeEvent ev)

// zmienione na

static void Program_OnEvent1(object sender, EventArgs<SomeEvent> ev)

Ta druga forma (func(object sender, CośtamEventArgs e)) jest pewną konwencją w .NET, więc chyba nie ma nic złego w stosowaniu jej? Jeśli chcesz przypisywać metody bezpośrednio możesz to zrobić na 2 sposoby - prostym wrapperem (ręcznym tworzeniem eventHandlera zamiast korzystanie z gotowego Eventhandler<T>), albo jeszcze prościej -

Something<SomeEvent>.OnEvent += (sender, e) => Program_OnEvent1(e.Data);

I ostatnia zmiana, chyba najpoważniejsza - zmieniło się działanie kodu. To znaczy, u Ciebie

Something<SomeEvent>.OnEvent += Program_OnEvent1;
Something<SomeEvent>.OnEvent += Program_OnEvent1;

Powodowało dodanie jednego eventu - to celowe działanie czy przypadek (nie napisałeś o tym w poście)?
U mnie powoduje to dodanie dwóch eventów (a później usunięcie jednego powoduje że zostaje drugi) - IMO to bardziej naturalne, ale to też możesz zmienić, w C# wszystko jest możliwe (za pomocą add{}remove{} na przykład) :>.

0

Problem z twoim rozwiązaniem jest taki, że straciłem w ten sposób swoją klasę statyczną - jest teraz tyle instancji, co użytych zostanie IEvent-ów jako parametrów generycznych.

2

Przeszkadza Ci dodawanie eventów przez funkcję Addhandler i RemoveHandler? Jeśli tak - nie ma problemu, można prosto zmienić. Jeśli nie, zmieniłem na takie coś:

public interface IEvent { }

public struct SomeEvent : IEvent { }
public struct AnotherEvent : IEvent { }

class EventArgs<T> : EventArgs
{
    public EventArgs(T data)
    {
        this.Data = data;
    }

    public T Data { get; private set; }
}

static class Something
{
    static Dictionary<Type, List<object>> events = new Dictionary<Type, List<object>>();

    public static void RegisterHandler<T>(EventHandler<EventArgs<T>> handler)
        where T : IEvent
    {
        if (!events.ContainsKey(typeof(T))) { events[typeof(T)] = new List<object>(); }
        events[typeof(T)].Add(handler);
    }

    public static void RemoveHandler<T>(EventHandler<EventArgs<T>> handler)
        where T : IEvent
    {
        events[typeof(T)].Remove(handler);
    }

    public static void Invoke<T>(T @event)
        where T : IEvent
    {
        foreach (EventHandler<EventArgs<T>> handler in events[typeof(T)])
        { handler(null, new EventArgs<T>(@event)); }
    }
}

class Program
{
    static void Main(string[] args)
    {
        Something.RegisterHandler<SomeEvent>(Program_OnEvent1);
        Something.RegisterHandler<SomeEvent>(Program_OnEvent1);
        Something.RegisterHandler<AnotherEvent>(Program_OnEvent1);
        Something.RegisterHandler<SomeEvent>(Program_OnEvent2);
        Something.Invoke(new SomeEvent());

        Something.RemoveHandler<SomeEvent>(Program_OnEvent1);
        Console.WriteLine();

        Something.Invoke(new SomeEvent());
        Console.WriteLine();

        Something.Invoke(new AnotherEvent());
        Console.ReadKey();
    }

    static void Program_OnEvent1(object sender, EventArgs<AnotherEvent> ev)
    { Console.WriteLine("Program_OnEvent1 - AnotherType"); }

    static void Program_OnEvent1(object sender, EventArgs<SomeEvent> ev)
    { Console.WriteLine("Program_OnEvent1 - SomeEvent"); }

    static void Program_OnEvent2(object sender, EventArgs<SomeEvent> ev)
    { Console.WriteLine("Program_OnEvent2 - SomeEvent"); }
}
0

Wolałbym faktyczne zdarzenia, ale podsunąłeś mi fantastyczny pomysł, który o dwa rzędy wielkości upraszcza problem - różne event handlery można trzymać razem jako object ;).

using System;
using System.Collections.Generic;
using System.Linq;

namespace Rev.GenericEventsTest
{
    public interface IEvent { }

    public struct SomeEvent : IEvent { }
    public struct AnotherEvent : IEvent { }

    public static class Something
    {
        private static Dictionary<Type, List<object>> _events = new Dictionary<Type, List<object>>();

        private static void RegisterHandler<TEvent>(Subscriber<TEvent>.SomeEventHandler eventHandler)
            where TEvent : IEvent
        {
            Type type = typeof(TEvent);

            if (!_events.ContainsKey(type))
                _events.Add(type, new List<object>());

            _events[type].Add(eventHandler);
        }

        private static void UnregisterHandler<TEvent>(Subscriber<TEvent>.SomeEventHandler eventHandler)
            where TEvent : IEvent
        {
            Type type = typeof(TEvent);

            if (_events.ContainsKey(type))
                _events[type].Remove(eventHandler);
        }

        public static void Raise<TEvent>(TEvent @event)
            where TEvent : IEvent
        {
            Type type = typeof(TEvent);

            if (_events.ContainsKey(type))
            {
                foreach (Subscriber<TEvent>.SomeEventHandler eventHandler in _events[type])
                    eventHandler(@event);
            }
        }

        public static class Subscriber<TEvent> where TEvent : IEvent
        {
            public delegate void SomeEventHandler(TEvent @event);

            public static event SomeEventHandler OnEvent
            {
                add
                {
                    RegisterHandler<TEvent>(value);
                }

                remove
                {
                    UnregisterHandler<TEvent>(value);
                }
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Something.Subscriber<SomeEvent>.OnEvent += Program_OnEvent1;
            Something.Subscriber<SomeEvent>.OnEvent += Program_OnEvent1;
            Something.Subscriber<AnotherEvent>.OnEvent += Program_OnEvent1;
            Something.Subscriber<SomeEvent>.OnEvent += Program_OnEvent2;

            Something.Raise<SomeEvent>(new SomeEvent());

            Something.Subscriber<SomeEvent>.OnEvent -= Program_OnEvent1;

            Console.WriteLine();

             Something.Raise<SomeEvent>(new SomeEvent());

            Console.WriteLine();

             Something.Raise<AnotherEvent>(new AnotherEvent());
        }

        static void Program_OnEvent1(AnotherEvent ev)
        {
            Console.WriteLine("Program_OnEvent1 - AnotherType");
        }

        static void Program_OnEvent1(SomeEvent ev)
        {
            Console.WriteLine("Program_OnEvent1 - SomeEvent");
        }

        static void Program_OnEvent2(SomeEvent ev)
        {
            Console.WriteLine("Program_OnEvent2 - SomeEvent");
        }

    }
}

Wydaje mi się, że będzie ciężko coś jeszcze w tym kodzie uprościć, z takiej formy jestem już zadowolony.

0

Jeśli potrzebujesz czegoś działającego to polecam: http://msdn.microsoft.com/en-us/library/ff921122(v=pandp.20).aspx

1 użytkowników online, w tym zalogowanych: 0, gości: 1