Podczas pisania prostego tool’a do komunikacji po porcie szeregowym natknąłem się na cechę C#, której dotąd nie znałem. Mowa tutaj o akcesorach zdarzeń (ang. event accessors) add i remove dla zdarzeń. Przeczytałem wiele różnych artykułów na temat różnych mechanizmów tego języka, widziałem wiele “żywego” kodu, jednak nie przypominam sobie żebym się wcześniej gdzieś z tym spotkał. Ok, tyle tytułem wstępu a teraz do rzeczy.
Z reguły korzystając z mechanizmu zdarzeń używa się operatorów przypisania, tak jak poniżej:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
namespace EventAccessors.Example1 { class Message { public event EventHandler OnMessageReceived; // some logic here... } class Notifier { // some logic here... public void RefreshNotificationList(object sender, EventArgs eventArgs) { // some logic here... } } static class Program { public static void Main() { var message = new Message(); var notifier = new Notifier(); // bind event handle message.OnMessageReceived += new EventHandler(notifier.RefreshNotificationList); // remove event handle message.OnMessageReceived -= new EventHandler(notifier.RefreshNotificationList); } } } |
Powyższy kod jest raczej oczywisty i nie wymaga wyjaśnień. To rozwiązanie idealnie sprawdza się w dla większości zastosowań, jednak czasem może okazać się niewystarczające. Załóżmy, że tworzymy program, który ma nasłuchiwać na określonym porcie i w momencie otrzymania danych wyświetlić je w widoku. Dla uproszczenia przyjmijmy, że mamy 3 klasy: Port, Communication, Gui. Zadaniem klasy Port jest obsługa portu, klasa Communication odpowiada za komunikację na wybranym porcie, natomiast klasa Gui symuluje widok. Zależności pomiędzy klasami najlepiej zobrazuje przykład:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
namespace EventAccessors.Example2 { public class Port { public event EventHandler OnDataReceived; public void Open() { // some logic here... } } class Communication { private readonly Port _port; public Communication(Port port) { _port = port; } // some logic here... } static class Program { class Gui { private Communication _communication; public Gui(Communication communication) { _communication = communication; } private void UpdateView(object sender, EventArgs eventArgs) { // some logic here... } } public static void Main() { var gui = new Gui(new Communication(new Port())); } } } |
W tym miejscu dotarłem do etapu, gdzie zaczynają się pojawiać problemy. Jak widać klasa Gui może jedynie korzystać z logiki komunikacji, co nie powinno budzić wątpliwości. Z drugiej strony klasa Port posiada zdarzenie, które jest wywoływane w momencie pojawienia się danych na wejściu. Problem jaki można tu zaobserwować jest następujacy: jak wydelegować obsługę zdarzenia OnDataReceived do klasy Gui jeżeli nie ma bezpośredniej możliwości podpięcia się pod zdarzenie?
Pierwszą rzeczą, którą należy zrobić jest dodanie analogicznego zdarzenia do klasy Communication. Uzasadnieniem dla takiego posunięcia jest fakt, że odbieranie danych jest integralną częścią komunikacji. Po takim zabiegu jesteśmy już w stanie wpiąć metodę odświeżającą GUI do zdarzenia. Korzystając z mechanizmu, przedstawionego na początku przykład po obu zabiegach wygląda następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
namespace EventAccessors.Example3 { public class Port { public event EventHandler OnDataReceived; public void Open() { // some logic here... } } class Communication { private readonly Port _port; public event EventHandler OnDataReceived; public Communication(Port port) { _port = port; } // some logic here... } static class Program { class Gui { private Communication _communication; public Gui(Communication communication) { _communication = communication; _communication.OnDataReceived += new EventHandler(UpdateView); } private void UpdateView(object sender, EventArgs eventArgs) { // some logic here... } } public static void Main() { var gui = new Gui(new Communication(new Port())); } } } |
Na pierwszy rzut oka może się wydawać, że problem został rozwiązany. Niestety gdyby program został uruchomiony okazałoby się, że tak naprawdę GUI pozostaje bez zmian, wpięty debugger pokazałby, że metoda UpdateView nie jest wywoływana. Dlaczego tak jest? Jest tak dlatego, że zdarzenia zdefiniowane w klasach Communication i Port to dwie kopie. W takim układzie obsługa zdarzenia została wpięta do zdarzenia, które nigdy nie jest wywoływane. Właśnie w tym momencie przychodzą z pomocą akcesory add i remove zawarte w tytule. Omawiane akcesory pozwalają na wstrzyknięcie dodatkowej logiki w momencie dodawania, bądź usuwania obsługi zdarzenia. W naszym przypadku służą do przekazania referencji do metody z klasy Gui do zdarzenia w klasie Port. Dzięki temu otrzymaliśmy zamierzony efekt, co bez użycia add i remove byłoby dużo trudniejsze (uwierz, najpierw rozwiązałem problem klasycznie, ale efekt mi się nie podobał i zacząłem kombinować dalej).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
namespace EventAccessors.Example4 { public class Port { public event EventHandler OnDataReceived; public void Open() { // some logic here... } } class Communication { private readonly Port _port; public event EventHandler OnDataReceived { add { _port.OnDataReceived += value; } remove { _port.OnDataReceived -= value; } } public Communication(Port port) { _port = port; } // some logic here... } static class Program { class Gui { private Communication _communication; public Gui(Communication communication) { _communication = communication; _communication.OnDataReceived += new EventHandler(UpdateView); } private void UpdateView(object sender, EventArgs eventArgs) { // some logic here... } } public static void Main() { var gui = new Gui(new Communication(new Port())); } } } |
Przedstawiony przeze mnie przykład jest oczywiście jednym z wielu pomysłów wykorzystania tego mechanizmu. Mechanizm sprawdziłbym się również w przypadku, kiedy chcielibyśmy logować informacje dotyczące rejestrowania i wyrejestrowania obsługi zdarzeń. Jasne, że możemy takie informacje zbierać zaraz po wykonaniu przypisania +=/-=, ale jeżeli mamy dużą liczbę liczbę metod, które obsługują śledzone zdarzenie to w efekcie kod odpowiedzialny za logowanie tych informacji musielibyśmy skopiować tyle razy ile mamy przypisań (ewidentne naruszenie zasady DRY). Dużo lepiej w takim przypadku zrobić to centralnie przy użyciu add i remove. Szerzej na temat omawianych akcesorów można poczytać tutaj.
Zastanawiam się czy spotkałeś się wcześniej z tym mechanizmem? Wydaje mi się, że jest dość rzadko używany ze względu na stosunkowo niewielką liczbę zastosowań (przynajmniej takie wrażenie odnoszę w tym momencie), ponieważ w wielu przypadkach wystarcza klasyczne przypisanie. Jeżeli znasz jakieś ciekawe zastosowania to pisz śmiało :).
Pingback: dotnetomaniak.pl
Przykład wydaje mi się nadmiernie przebudowany. Ale OK. Akcesory add i remove wykorzystywało się swojego czasu do implementacji wzorca WeakEvent. Kolejnym scenariuszem w którym z nich korzystam jest powiadamianie “spóźnialskich” obserwatorów – takich którzy zostają podpięciu po nastąpieniu interesujących . Podnoszę event w akcesorze add jeżeli zdarzenie nastąpiło już w przeszłości, nie jest to wzorzec godny naśladowania ale czasami jest najprostszym możliwym rozwiązaniem.
Duży kaliber przykładu to wynik maksymalnego uproszczenia problemu z jakim się faktycznie zetknąłem. Nie miałem pomysłu na prostszy przykład dlatego poszedłem w tę stronę.
Witam
Dużo przed tobą więc szkól się szkól. 🙂
1. Masz literówkę(powinno być):
// remove event handle
message.OnMessageReceived -= new EventHandler(nofifier.RefreshNotificationList);
A jeszcze lepiej to:
EventHandler handler = (s, e) =>
{
// coś tam
};
message.OnMessageReceived += handler;
message.OnMessageReceived -= handler;
Gdy chcemy podpiąć się raz i odpiąć to takie coś ma zastosowanie:
EventHandler handler = null;
handler = (s, e) =>
{
// coś tam
message.OnMessageReceived -= handler;
};
message.OnMessageReceived += handler;
To wszystko oczywiście nijak się ma do tematu. 😀
Dzięki za wychwycenie literówki, już poprawiłem :). Co do lambda expression, to ja to traktuję jako alternatywna forma podpięcia obsługi zdarzenia uzasadniona w pewnych sytuacjach. Jeżeli mamy pewien zbiór operacji opakowanych w jedną metodę, w tym przypadku
RefreshNotificationList
, to nie widzę potrzeby użycia lambda (ew. w celu uproszczenia zapisu). Lambda użyłbym np. chcąc podpiąć logowanie informacji o zdarzeniu, o tak:EventHandler handler = (s, e) =>
{
_logger.Info(...);
};
message.OnMessageReceived += handler;
message.OnMessageReceived -= handler;
Wtedy lambda sprawdza się idealnie, bo wiadomo, że bez sensu byłoby stworzenie specjalnej metody dla wywołania loggera :). Oczywiście, jest jeszcze trzecia alternatywa w postaci metod anonimowych, co prezentuje się następująco:
EventHandler handler = delegate(s, e)
{
// coś tam
};
message.OnMessageReceived += handler;
message.OnMessageReceived -= handler;
W zasadzie nie wiem, czy miedzy metodami anonimowymi a lambda jest jakaś istotna różnica więc wydaje mi się, że wybór pomiędzy jednym a drugim to jest kwestia własnych preferencji i konkretnego przypadku.