Powiadomienia o nowych zamówieniach w czasie rzeczywistym – pierwsze kroki z SignalR

By | May 8, 2016

Chciałbym, aby po złożeniu zamówienia przez klienta obsługa restauracji była automatycznie powiadamiana o nowym zamówieniu i mogła rozpocząć jego realizację od razu. Bez odświeżania okna, szukania nowości na liście zamówień. Chcemy mieć wielki komunikat na środku ekranu niezależnie od tego gdzie w panelu administracyjnym właśnie się znajdujemy (przyjąłem założenie, że realizacja zamówienia ma najwyższy priorytet) i chcemy otrzymać tą informację natychmiast. Do tego celu idealnie nadaje się biblioteka SignalR i absolutne podstawy korzystania z niej chciałbym opisać w poniższym poście.

Zacznijmy od tego czym jest SignalR. To w wielkim skrócie biblioteka, która pozwala aplikacji uruchomionej na naszym serwerze wysyłać powiadomienia wszystkim lub wybranym klientom, którzy “nasłuchują” w oczekiwaniu na “sygnał”, że coś się właśnie wydarzyło. Niejako drugą częścią biblioteki są moduły dla klientów/konsumentów, których zadaniem jest prowadzenie wspomnianego “nasłuchu”, w moim przypadku będzie to biblioteka wykorzystująca jQuery, ale możliwości odbioru powiadomień są ogromne i mamy dostępne komponenty dla WPF, WinForms, systemów nie-Microsoftowych etc. Tyle w telegraficznym skrócie, jeśli ktoś chce zapoznać się z szerszą definicją czy szczegółami technicznymi, to muszę go zaprosić na piękną stronę, której nazwa zaczyna się na literę G.

Dlaczego SignalR?

Bo działa sprawnie, szybko i uruchomienie podstawowej funkcjonalności przy jego wykorzystaniu jest banalnie proste. Wczoraj skorzystałem z niego po raz pierwszy, testową wersję powiadomień do panelu administracyjnego ogarnąłem w jakąś godzinę, a jak tylko poczytałem o bibliotece nieco więcej i wgłębiłem się w temat, to przyszło mi do głowy kilka innych funkcjonalności. Dlatego też dzisiejszy post jest tylko wprowadzeniem i są w nim banały, a odrobinę bardziej skomplikowane rzeczy omówię następnym razem.

Instalacja i konfiguracja

Instalacja jest prosta, bo wymaga tylko i wyłącznie instalacji NuGeta – Microsoft.AspNet.SignalR

Nie bawiłem się jeszcze w Dependency Injection wraz z komponentami omawianej biblioteki, więc ciężko powiedzieć mi coś więcej na ten temat w obecnej chwili, ale Autofac będzie wymagał dodatkowej uwagi jeśli będziemy chcieli, by te dwie biblioteki połączyły siły w naszym projekcie. Po szczegóły póki co zapraszam do dokumentacji Autofaca.

W moim przypadku konfiguracja przebiegła bardzo szybko. Zacząłem od utworzenia katalogu Hubs, w projekcie YumYum.UI. Znalazła się w nim pierwsza klasa dziedzicząca po klasie Hub, którą stworzyłem klikając RMB na nowo powstałym katalogu i zaznaczając opcję Add Item (lub zaznaczając katalog LMB i używając skrótu Ctrl + Shift + A), ze znanej Wam listy wystarczy wybrać SignalR Hub Class i gotowe.

Huby są centrum naszej serwerowej części wysyłania powiadomień. We frontendzie wywoływane są metody, które są zdefiniowane w Hubach, trafiają do nich, zostają przemielone, a odpowiedzi są wysyłane do zdefiniowanych odbiorców i trafiają do każdego, kto akurat “nasłuchuje”, gdzie kod jQuery wykonuje wszystko co mu kazaliśmy po otrzymaniu “sygnału”. Mój Hub zawiera póki co jedną metodę i wygląda tak:

Ale aby coś w ogóle zaczęło działać musimy do projektu dodać jeszcze jedną maleńką klasę o nazwie Startup.cs.

Jej zadanie jest bardzo podobne do mapowania routingu i ma za zadanie przygotowanie zawartości katalogu ~/signalr/, który zawiera dynamicznie generowany kod JavaScriptu łączący stronę klienta ze skonfigurowanymi przez nas Hubami.

Po tej podstawowej konfiguracji powinniśmy odpalić nasz projekt i udać się pod adres swojej aplikacji ze ścieżką ~/signalr/hubs . W moim przypadku jest to: http://localhost:61845/signalr/hubs . Powinniśmy tam zobaczyć sporo kodu JS zaczynającego się od nagłówka:

Jeśli tak jest, to wszystko działa prawidłowo i możemy przejść do wysyłania prostych powiadomień.

Implementacja w widokach

Póki co nie wdrożyłem jeszcze wszystkiego tak jak zamierzam, bo muszę odrobinę przebudować proces składania zamówienia i po jego złożeniu przekierować klienta na stronę z podsumowaniem, statusem zamówienia(który ma być uaktualniany przez SignalR właśnie) i ewentualnie po drodze zahaczyć o stronę z płatnością elektroniczną jeśli została wybrana taka opcja. Póki co powiadomienie jest wysyłane po kliknięciu w przycisk “Złóż zamówienie” jeszcze przed walidacją poprawności wypełnionych pól, co byłoby kompletnie bez sensu i generowałoby mnóstwo niepotrzebnych powiadomień. Kod, który odpowiada za wysłanie powiadomienia jest widoczny tylko i wyłącznie w widoku, z którego jest wysyłany. Do Heada jest wysyłany przy wykorzystaniu sekcji. Wygląda on tak:

Bardzo ważne jest to, by oprócz ścieżki do biblioteki jQuery SignalR dodać referencję do ~/signalr/hubs . Co się tam dzieje opisałem pokrótce nieco wyżej. Teraz przeanalizujmy kolejne linijki.

W linii tej tworzymy zmienną orderHub, której przypisujemy połączenie z klasą OrdersHub, którą utworzyłem na początku, połączenie to zostało wygenerowane właśnie w ~/signalr/hubs i odwołujemy się do niego za pośrednictwem $.connection.ordersHub. Warto zwrócić uwagę na wielkość liter w klasie OrdersHub i $.connection.ordersHub. Jeśli nie użyjemy odpowiedniego atrybutu ([HubName]) w klasie Huba, to nazwa klasy zostanie zmieniona i zapisana za pomocą camelCase notation (pierwsza litera będzie mała).

 

Tutaj przechodzimy już do wysyłania faktycznego powiadomienia. Pierwsza linia negocjuje i otwiera połączenie z hubem, a fragment done sprawia, że reszta kodu oczekuje aż połączenie zostanie zakończone sukcesem zanim wykona się reszta kodu. Potem wykorzystując jQuery przypinamy pod odpowiedni button funkcję orderHub.server.newOrder(1);, orderHub jest zmienną, którą zdefiniowaliśmy wcześniej, server oznacza wywołanie metody z klasy Huba, a newOrder wywołuje metodę NewOrder z Huba z parametrem 1 (który zostanie zmieniony). Ponownie warto zwrócić uwagę na zmianę na camelCase.

Co właściwie się stało? Potwierdziliśmy zamówienie, a przy okazji wywołaliśmy metodę NewOrder z naszego Huba. W tej chwili składa się ona z jednej linii kodu. Dla przypomnienia: 

Do wszystkich klientów połączonych z Hubem zostaje wysłane “zdarzenie” z parametrem id, który dostaliśmy na wejściu. Warto zaznaczyć, że wszystkie dane przesyłane są w formacie JSON i są automatycznie serializowane i deserializowane, co pozwala na używanie typów złożonych, tablic i innych, bardziej rozbudowanych danych. Nie należy jednak przesadzać i wysyłać dużej ilości danych, bo nie do tego służy SignalR. O tym jak planuję ograniczyć ich ilość napiszę pod koniec, a sam temat rozwinę w kolejnym poście za jakiś czas.

Przejdźmy do tego jak odbierane jest powiadomienie o nowym zamówieniu. Całość znajduje się bezpośrednio w sekcji Head Layoutu odpowiadającego za cały układ panelu administracyjnego. Zdecydowałem się na taki zabieg, by powiadomienie było widoczne niezależnie od tego gdzie akurat jest obsługa i czym aktualnie się zajmuje. Znaczniki <script> wyglądają dokładnie tak samo jak poprzednim razem, różni się jedynie kod JS, który wygląda tak:

Po raz kolejny więc tworzymy zmienną orderHub, w następnej linii otwieramy połączenie, a potem rozpoczynamy “nasłuch” dla addNewOrder jako client po otrzymaniu informacji o tym zdarzeniu z huba wywoływana jest zdjefiniowana funkcja, która pokazuje okno z powiadomieniem i dodaje w umieszczonej tam tabeli jeden wiersz wypełniony póki co nic nie znaczącą treścią.

Warto zauważyć, że linia orderHub.client.addNewOrder różni się od użytej poprzednio orderHub.server.newOrder(1);. Jedna z nich jest klientem dla metody wywołanej w Hubie, druga wywołuje metodę w Hubie na serwerze. 

Co dalej?

Funkcjonalność, o której pisałem zostanie nieco rozbudowana i będzie pokazywać w oknie powiadomienia tylko niezbędne szczegóły. Ale nie sądzę, by dobrym pomysłem było wysyłanie ich wszystkich za pomocą SignalR. Widzę to w ten sposób, że aby nieco oszczędzić zasoby wysyłałbym tylko id nowo utworzonego zamówienia (lub zapisywany w bazie GUID ze względów bezpieczeństwa), a po otrzymaniu informacji, że pojawiło się nowe zamówienie o takim i takim ID, asynchronicznie użyłbym JS do wywołania akcji i zwrócenia Partial View (lub może JSONa otrzymanego z WEB API) ze szczegółami zamówienia do wyświetlenia w pokazywanym oknie. Muszę jeszcze dokładniej przemyśleć całą sprawę.

Drugą funkcjonalnością o jakiej myślę i do której SignalR nada się idealnie to ekran z podsumowaniem zamówienia. Jeśli zamawiasz jedzenie online to pewnie kojarzysz licznik czasu odliczający do pięknego momentu, kiedy w mieszkaniu rozbrzmi wspaniały dźwięk dzwonka do drzwi, a po szaleńczym pędzie do drzwi dowiadujesz się, że właśnie odwiedzili Cię Świadkowie Jehowy lub ankieterzy.

Otóż chciałbym aby klient mógł na ekranie podsumowującym zamówienie zobaczyć co się z nim dzieje, za ile szacunkowo powinno znaleźć się u niego, a jeśli nie zostało zaakceptowane przez powiedzmy 10 minut od złożenia, to zamiast informacji “Twoje zamówienie zostało zaakceptowane i powinno być za około 30 minut” powinna zostać wyświetlana informacja “Minęło 10 minut, a Twoje zamówienie wciąż nie zostało zaakceptowane. Skontaktuj się z nami pod numerem telefonu 123456, by upewnić się, że wszystko jest w porządku”. Co prawda tylko 2 razy w życiu zdarzyło mi się, że zamówienie złożone online nie trafiło do celu, ale po ponad 1,5h oczekiwania i informacji otrzymanej gdy już zadzwoniłem, że coś się popsuło i nic nie wiedzą o jedzeniu dla mnie, można się delikatnie zdenerwować.

Już dawno nauczyłem się, że w zakupach online klient dobrze poinformowany, to klient spokojny i zadowolony. Ma to szczególne znaczenie przy dowozie posiłków gdzie 2 godziny to już długi czas oczekiwania. SignalR wydaje mi się idealnym rozwiązaniem, które przy odpowiednim wykorzystaniu może informować klienta o tym co dzieje się z jego zamówieniem na poziomie panelu admina. Ale o tym za jakiś czas, bo chociaż biblioteka ta mnie zafascynowała, to chciałbym zakończyć podstawową integrację z systemem płatności PayU.

Podbij ↑