Jak dynamicznie zbudować metodę do obsługi zdarzenia na podstawie EventInfo

0

Cześć,

potrzebuję rozwiązania, które umożliwi mi dynamiczne utworzenie metody do obsługi zdarzenia na podstawie danych z EventInfo. Event handler to Task i chciałbym przepchać wszystkie argumenty utworzonej metody po wystąpieniu zdarzenia z użyciem kodu dalej, ale generalnie utknąłem na etapie budowy takiego handlera.

Na razie testowo zrobiłem 5 metod jak poniżej:

        private Task Handler()
        {
            return UniversalHandler();
        }

        private Task Handler1(object arg1)
        {
            return UniversalHandler(arg1);
        }

        private Task Handler2(object arg1, object arg2)
        {
            return UniversalHandler(arg1, arg2);
        }

        private Task Handler3(object arg1, object arg2, object arg3)
        {
            return UniversalHandler(arg1, arg2, arg3);
        }

        private Task Handler4(object arg1, object arg2, object arg3, object arg4)
        {
            return UniversalHandler(arg1, arg2, arg3, arg4);
        }

        private Task Handler5(object arg1, object arg2, object arg3, object arg4, object arg5)
        {
            return UniversalHandler(arg1, arg2, arg3, arg4, arg5);
        }

        private Task UniversalHandler(params object[] args)
        {
            // Mój kod
            return Task.CompletedTask;
        }

Subskrypcję dodaję z użyciem switch case zależnie od liczby argumentów wyciągniętych z EventInfo.

            switch (amountOfArgs)
            {
                case 1:
                    eventInfo.AddEventHandler(instance, Handler1);
                    SetRemoveHandlerAction(eventInfo, Handler1, instance);
                    break;

Kod powyżej działa bez zarzutu z argumentami niegenerycznymi. Oczwiście nie zadziała z generycznymi no i nie mogę mieć pewności, że w przyszłości nie będę potrzebował obsługi np. 6, albo 10 argumentów. Oczywiście jest to mało prawdopodobne, ale niedosyt, że to tak wygląda pozostaje.

Kod jest wpięty do zbudowanego diagramu. Mam bloki, ktore reprezentują klasy. Po dodaniu bloku reprezentującym instancję danej klasy, można ją wpiąć do bloku odpowiedzialnego za wyciąganie zdarzeń, a wybrane zdarzenie wpiąć do bloku generowania argumentów. Argumenty można wpiąć dalej, np do bloków funkcyjnych lub innych klas. W momencie wystąpienia zdarzenia, zbudowana w diagramie logika wykonuje się.

Wszystko zdaje się działać do momentu wpięcia zdarzenia z argumentem np. Myclass<MyOtherClass,int> arg. Tu kod się wysypie bo AddEventHandler oczywiście nie zadziała.

Nie chciałem budować zbyt wielu bloków do diagramu ręcznie, więc większość jest zbudowana przez reflection. Niestety myślałem, że z eventami będzie mi najłatwiej, ale okazało się, że było z nimi najwięcej zachodu. To jest jedyny mankament, który odłożyłem na późniem, bo nie mogę sobie z nim porawdzić. Próbowałem użyć:

Delegate.CreateDelegate()

Niestety on potrzebuje MethodInfo, a ja przecież nie mam metody. Myślałem więc o DynamicMethod, ale w ogole nie mam pojęcia jak tam wepchnąć body z kodem. Może ktoś coś poradzi, albo odeśle w jakieś sensowne miejsce gdzie coś podobnego jest na jakimś samplu, bo za długo już grzebię w Internecie i szukam, aktoś mógł coś podobnego ogarniać wcześniej lub gdzieś się na to natknął. Dzięki z góry za pomoc i pozdrawiam :)

1

Szczerze to nie chce mi się tego wszystkiego czytać ani wgryzać się w to o co ci chodzi, ale obstawiam na 90% że szukasz expression trees:
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/expression-trees/

0

@obscurity: może się mylę, ale nie wydaje mi się żeby to miało zastosowanie w przypadku mojego problemu.

// Compile and execute an expression tree.  
int factorial = Expression.Lambda<Func<int, int>>(block, value).Compile()(5);  

Skąd mam wiedzieć, że tu będzie int, int?

Może prościej będzie zobrazować co chcę osiągnąć tak:

Normalnie gdybym trzymał w kodzie jawnie instancję klasy ze zdarzeniem przepchnąłbym te zdarzenia w taki sposób.

myClassInstance.SomeEvent += (arg1, arg2Generic, arg3) => UniversalHandler(arg1, arg2Generic, arg3);
myClassInstance.SomeOtherEvent += (arg1, arg2, arg3) => => UniversalHandler(arg1, arg2, arg3);
myOtherClassInstance.SomeOtherEventXYZ += (arg1, arg2, arg3Generic) => UniversalHandler(arg1, arg2, arg3ThaCanBeGeneric);
...

 private Task UniversalHandler(params object[] args)
        {
            foreach(var arg in args)
               // Do something I want
    
            return Task.CompletedTask;
        }

To co wyżej chcę osiągnąć mając tylko EventInfo i instancję klasy w której są te zdarzenia w formie object, który został wstrzyknięty do klasy za pomocą innej metody i tylko na chwilę. W momencie wystąpienia w tym obiekcie zdarzenia, ma się wykonać metoda UniversalHandler.

Zaczynam się zastanawiać czy to co chcę zrobić jest w ogóle możliwe. Warto dodać że apka jest na .Net 6.

0

Czemu masz pierdyliard przeciążeń z dużą ilością argumentów? Miej jeden argument z klasy EventArgs. I nowe klasy argumentów dziedzicz po tym. Czy to nie wchodzi w Twój problem? Co konkretnie chcesz osiągnać? Jaki masz problem do rozwiązania? (nie oczekuję odpowiedzi: muszę utworzyć handlera mając EventInfo, tylko opisu konkretnego problemu)

2
Wojciech Dudek napisał(a):

@obscurity: może się mylę, ale nie wydaje mi się żeby to miało zastosowanie w przypadku mojego problemu.

// Compile and execute an expression tree.  
int factorial = Expression.Lambda<Func<int, int>>(block, value).Compile()(5);  

Skąd mam wiedzieć, że tu będzie int, int?

mylisz się. Expression trees pozwala dynamicznie wygenerować dowolny kod, jeśli jesteś w stanie go zapisać samemu to jesteś w stanie to zrobić za pomocą expression trees.
Lambdę możesz też utworzysz przez Lamba z listą parametrów
listę parametrów znasz na podstawie (EventInfo).EventHandlerType.GetGenericArguments()

0

@Juhas:

Czemu masz pierdyliard przeciążeń z dużą ilością argumentów?

Do testu. Napisałem w pierwszym poście. Chciałem przetestować czy reszta kodu działa (jesli masz na myśli przykładowe metody z pierwszego postu).

Miej jeden argument z klasy EventArgs

Nie mogę. Kod nie jest mój. Sposób budowy argumentów od tych eventów wynika z architektury biblioteki, która jest wpięta pośrednio do mojego kodu. Nie mogę mieć jednego argumentu. Muszę przyjąc ich tyle ile jest na dany event potrzebne.

Co konkretnie chcesz osiągnać? Jaki masz problem do rozwiązania?

Napisałem. Nie będę przecież litani pisał na temat programu, tego co robi i jak dokładnie i szczegółowo działa. Problem myślę jest w miarę jasny, to tylko maleńka część aplikacji. Przeczytaj proszę pierwszy post jeszcze raz. @obscurity ogarnął choć nie czytał całości, to i Ty możesz, jeśli faktycznie chcesz pomóc.

@obscurit spróbuję pokombinować wieczorem i dam znać czy się udało, dzięki za info.

2

masz tu gotowca bo miałem chwilę czasu i rzadko jest okazja pobawić się takimi rzeczami

using static System.Linq.Expressions.Expression;

var instance = new TestClass();
var eventInfo = instance.GetType().GetEvent("SomeOtherEvent");

// wyciąga listę parametrów z metody `Invoke` - to działa zarówno dla eventów typu EventHandler jak i generycznych Action<...>
var eventArguments = eventInfo.EventHandlerType.GetMethod(nameof(EventHandler.Invoke)).GetParameters();
var parameters = eventArguments.Select(param => Parameter(param.ParameterType)).ToArray();
var universalHandler = typeof(Program).GetMethod(nameof(UniversalHandler));

// tworzy lambdę (EventHandler)(x, y, ...) => UniversalHandler(new[] { (object)x, (object)y, (object)... })
var handler = Lambda(eventInfo.EventHandlerType,
        Call(universalHandler,
            NewArrayInit(typeof(object),
                parameters.Select(
                    param => Convert(param, typeof(object)))
            )
        ), parameters)
    .Compile();

eventInfo.AddEventHandler(instance, handler);

ciekawi mnie tylko co sensownego możesz zrobić w UniversalHandler nie znając liczby ani typu argumentów

0

Dzięki wielkie! Po drobnych przeróbkach działa, no i troszkę lepiej mi się te całe Expressions układają w głowie. Może nawet pozwoli mi to trochę w niektórych miejscach uprościć kod po zakończeniu fazy prototypowania.

Zaspokajając ciekawość.

Robię diagram przepływu, który teoretycznie powinien umożliwić budowę prostej logiki do dowlnego celu w oparciu o zasoby z określonej biblioteki. Oczywiście każda raczej będzie przygotowana przeze mnie oddzielnie w formie aplikacji. Ważne bym miał backend, któremu tylko nieznacznie trzeba pomóc w obsłudze danej biblioteki. Np. w tym wypadku testowo do budowy bota do Discorda. Całość serializujesz i zapisujesz w formie projektu, który wczytujesz do odrębnej dedykowanej apki, która bez całej tej front-endowej otoczki realizuje zadanie, na jakiejś maszynce. W ten sposób w kótkim czasie będę mógł zrealizowac 10 podobnych aplikacji do różnych celów wpinając tylko co potrzebne do projektu, a użytkownik będzie w stanie postawić sobie bota, czy do czego tam dana apka będzie przeznaczona bez większej znajomości zagadnień programowania. Coś na wzór BluePrint w silniku Unreal Engine, tylko w dużo prostszej formie do rozbudowy w przyszłości.

To co zaznaczyłem na czerwono buduje się dynamicznie po wpięciu źródła do bloku, drugi czerwony bloczek jest zbudowany z pomocą tego co mi podałeś. Nie muszę wiedzieć co tam są za argumenty, ważne, że użytkownik może po nazwie, którą jeszcze muszę dopracować, bo raczej wszystko będzie tłumaczone, może wyłapać gdzie to wpiąć. Mam specjalnego jsona gdzie dodaje do wyjątków rzeczy których nie chcę w programie lub na odwrót, więc te bloczki generalnie będą mniejsze, ale z czasem odblokuje przetestowane funkcje by generalnie całość nie była zbyt okrojona.

Oczywiście jeszcze miesiące roboty, jak nie z rok, bo nie mam za dużo czasu po robocie na dalsze siedzenie przed komputerem :)

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