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:
1 2 3 4 5 6 7 8 9 10 11 12 |
using Microsoft.AspNet.SignalR; namespace YumYum.Hubs { public class OrdersHub : Hub { public void NewOrder(int id) { Clients.All.addNewOrder(id); } } } |
Ale aby coś w ogóle zaczęło działać musimy do projektu dodać jeszcze jedną maleńką klasę o nazwie Startup.cs.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using Owin; using Microsoft.Owin; [assembly: OwinStartup(typeof(YumYum.Startup))] namespace YumYum { public class Startup { public void Configuration(IAppBuilder appBuilder) { appBuilder.MapSignalR(); } } } |
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:
1 2 3 4 5 6 7 8 9 |
/*! * ASP.NET SignalR JavaScript Library v2.2.0 * http://signalr.net/ * * Copyright Microsoft Open Technologies, Inc. All rights reserved. * Licensed under the Apache 2.0 * https://github.com/SignalR/SignalR/blob/master/LICENSE.md * */ |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@section Scripts { <script src="~/Scripts/jquery.signalR-2.2.0.js"></script> <script src="~/signalr/hubs"></script> <script> $(function () { var orderHub = $.connection.ordersHub; $.connection.hub.start().done(function () { $("#newOrderBtn").click(function () { orderHub.server.newOrder(1); @*value to change*@ }); }); }); </script> } |
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.
1 |
var orderHub = $.connection.ordersHub; |
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).
1 2 3 4 5 |
$.connection.hub.start().done(function () { $("#newOrderBtn").click(function () { orderHub.server.newOrder(1); @*value to change*@ }); }); |
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:
1 2 3 4 |
public void NewOrder(int id) { Clients.All.addNewOrder(id); } |
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:
1 2 3 4 5 6 7 8 |
$(function () { var orderHub = $.connection.ordersHub; $.connection.hub.start(); orderHub.client.addNewOrder = function (id) { $("div.newOrderBox").show(); $("#boxContentTable").append("<tr><td><strong>Nowe zamówienie nr " + id + " od:</strong><td/><td>Imię</td><td>Nazwisko</td><td>Kwota</td></tr>"); }; }); |
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.