Category Archives: JavaScript

Przydatna biblioteka: JSON.NET

JSON.NET to darmowa i naprawdę prosta w użyciu biblioteka, która umożliwia parsowanie JSON z poziomu c#. Instalujemy ją oczywiście z NuGet:

image

Następnie API jest na tyle proste, że wystarczy tak naprawdę nam tylko IntelliSense:

var person = new Person() {FirstName = "Piotr", LastName = "Zielinski"};
string content = JsonConvert.SerializeObject(person);
Console.WriteLine(content);

Efektem będzie konwersja obiektu c# do JSON czyli:

{"FirstName":"Piotr","LastName":"Zielinski"}

Podobnie można dokonać deserializacji:

var person = new Person() {FirstName = "Piotr", LastName = "Zielinski"};
string content = JsonConvert.SerializeObject(person);

Person newPerson = JsonConvert.DeserializeObject<Person>(content);

Możliwe jest również manipulowanie na obiekcie JObject:

JObject person = JObject.Parse(@"{'FirstName':'Piotr','LastName':'Zielinski'}");

JObject eksponuje JSON i mamy do niego dostęp np. za pomącą indexer’a:

JObject person = JObject.Parse(@"{'FirstName':'Piotr','LastName':'Zielinski'}");
            Console.WriteLine(person["FirstName"]);

Jeśli dana właściwość to tablica to mamy dostęp do niej za pomocą standardowego indeksu:

JObject person = JObject.Parse(@"{'FirstName':'Piotr','LastName':'Zielinski','Numbers':['1','2','3']}");
Console.WriteLine(person["Numbers"][0]);
Console.WriteLine(person["Numbers"][1]);

Jeśli przyjrzyjmy się JObject, to zobaczymy, że jest tam wiele metod do odpytywania obiektu np.:

public IEnumerable<JToken> Descendants

JSON.NET wspiera również LINQ, co czyni wyszukiwanie danych bardzo łatwe (przykład z dokumentacji):

var categories =
    from c in rss["channel"]["item"].Children()["category"].Values<string>()
    group c by c
    into g
    orderby g.Count() descending
    select new { Category = g.Key, Count = g.Count() };

foreach (var c in categories)
{
   Console.WriteLine(c.Category + " - Count: " + c.Count);
}

Dzięki JSON.NET  możliwa jest konwersja pomiędzy XML a JSON:

XmlDocument xmlDocument=new XmlDocument();
xmlDocument.LoadXml("<Person><FirstName>Piotr</FirstName><LastName>Zielinski</LastName></Person>");
string jsonText = JsonConvert.SerializeXmlNode(xmlDocument);

Powyższe przykłady dotyczą przeszukiwania obiektów. Analogicznie jednak można je tworzyć tzn.:

JObject jObject=new JObject();
jObject["FirstName"] = "Piotr";
jObject["Numbers"]=new JArray("1","2","3");


Console.WriteLine(jObject.ToString());

Kod wygeneruje następujący JSON:

{
  "FirstName": "Piotr",
  "Numbers": [
    "1",
    "2",
    "3"
  ]
}

Od C# 4.0 mamy do dyspozycji dynamic dlatego ładniejszą składnie uzyskamy pisząc:

dynamic jObject=new JObject();
jObject.FirstName = "Piotr";
jObject.Numbers=new JArray("1","2","3");


Console.WriteLine(jObject.ToString());

SignalR: Wywoływanie metod z zewnętrznych klas

W poprzednich postach zakładaliśmy, że komunikacja między Hubem, a klientem (np. przeglądarką) odbywa się zawsze w wewnątrz hub’a,  w następnie wykonania metody serwerowej.

Bardzo często jednak jest potrzeba powiadomienia klienta po np. jakiś procesie biznesowym. Może to być automatycznie wykonywane zadanie czy po prostu akcja kontrolera.

Pierwsza próba, mogłaby wyglądać następująco:

var myFirstHub=new MyFirstHub();
myFirstHub.Clients.All.newNumber(number);

W ostatnim wpisie jednak napisałem, że instancja hub’a jest przechowywana przez SignalR i nie możemy sami manipulować nim. Powyższy kod zwróci zatem wyjątek InvalidOperationException:

Using a Hub instance not created by the HubPipeline is unsupported.

Prawidłowa inicjalizacja to zwrócenie kontekstu SignalR:

public ActionResult Increment(int number)
{                                   
  IHubContext hub=GlobalHost.ConnectionManager.GetHubContext<MyFirstHub>();
  hub.Clients.All.newNUmber(number);

  return View();
}

Jedna uwaga na koniec. Operacja GetHubContext może być czasochłonna i z tego względu, zawsze lepiej buforować kontekst a nie za każdym razem go tworzyć od nowa.

SignalR: Przekazywanie stanu

Obiekty Hub mają z góry określony czas życia. Ten sam obiekt nie jest współdzielony pomiędzy różne wywołania. Jeśli klient wywołuje metodę, instancja huba jest tworzona od nowa. Oznacza to, że nie możemy przechować stanu między zapytaniami w następujący sposób:

public class MyFirstHub : Hub
{
    private int _number = 0;

    public void Increment()
    {
        _number++;
        Clients.All.newNumber(_number);
    }
}

Na wyjściu takim sposobem, zawsze będzie jeden – za każdym wywołaniem instancja jest tworzona od nowa.

Pozostają inne opcje takie jak baza danych, zmienne statyczne itp. SignalR dostarcza jednak pewien mechanizm, który dla małej ilości danych jest dobry.

Każdy klient podłączony do Hub może przekazać swój stan w następujący sposób:

public class MyFirstHub : Hub
{    
   public void Increment()
   {
       Clients.Caller.number++;
       Clients.All.newNumber(Clients.Caller.number);
   }
}

Zmienna number w powyższy przykładzie to nasz stan. Z kolei Caller jak wcześniej, reprezentuje klienta, który wykonał daną metodę serwerową.

W JavaScript z kolei, możemy ustawić stan na dowolną wartość:

var myFirstHub = $.connection.myFirstHub;
myFirstHub.state.number = 0;

Oczywiście należy pamiętać, że cały stan, z każdym zapytaniem będzie wysyłany do serwera i potem zwracany. Z tego względu, lepiej ograniczyć się do małych zmiennych.

SignalR: wywoływanie metod

Dzisiaj trochę więcej szczegółów na temat, jak można wywoływać metody w Hub API. W ostatnim wpisie, zaimplementowaliśmy klasę, która wykonuje metodę po wszystkich klientach (broadcast). SignalR ma ogromne możliwości i istnieje wiele innych wzorców.

Dla przypomnienia broadcast wygląda następująco:

public void SendMessage(string message)
{
  Clients.All.newMessage(string.Format("{0}: {1}",DateTime.Now,message));
}

Clients posiada kilka metod, służących do wysyłania wiadomości w różnych sposób. Na przykład, aby wykonać metodę wyłącznie po stronie klienta, który wysłał zapytanie do serwera można:

public void SendMessage(string message)
{
  Clients.Caller.newMessage(string.Format("{0}: {1}",DateTime.Now,message));
}

Innymi słowy, gdy przeglądarka wywołuje SendMessage, newMessage zostanie wykonane wyłącznie w tej przeglądarce (a nie jak wcześniej we wszystkich podłączonych klientach).

Inną ciekawą konstrukcją jest wywołanie metody we wszystkich klientach, oprócz tego który zainicjował połączenie (wywołał metodę serwerową):

public void SendMessage(string message)
{
  Clients.Others.newMessage(string.Format("{0}: {1}",DateTime.Now,message));
}

Istnieje opcja wywołania metody dla określonego nazwą (string) połączenia:

Clients.Client("connectionID").newMessage(string.Format("{0}: {1}",DateTime.Now,message));

Analogicznie, wszystkie połączenia oprócz tych przekazanych jako parametr:

public void SendMessage(string message)
{
  Clients.AllExcept("connectionID1","connectionId2").newMessage(string.Format("{0}: {1}",DateTime.Now,message));
}

Jeśli znamy nazwę użytkownika (IUserIdProvider), możemy wywołać metodę po stronie klienta, który używa określonego loginu:

public void SendMessage(string message)
{
  Clients.User("user1").newMessage(string.Format("{0}: {1}",DateTime.Now,message));
}

Bardzo ciekawym rozwiązaniem są grupy. Umożliwiają one implementacje wzorca polegającym na publish\subscribe. Dzięki grupom, można wywoływać metody, wyłącznie w tych klientach, które nalezą do konkretnej grupy.

public void SendMessage(string message)
{
  Clients.Group("group_name").newMessage(string.Format("{0}: {1}",DateTime.Now,message));
}

Powyższy kod wywoła newMessage w klientach należących do group_name. Pozostaje pytanie, jak dodać użytkownika (połączenie) do grupy? Wystarczy:

public class MyFirstHub : Hub
{
   public void AddToGroup(string groupName)
   {
       Groups.Add(Context.ConnectionId, groupName);
   }
   public void RemoveFromGroup(string groupName)
   {
       Groups.Remove(Context.ConnectionId, groupName);
   }
}

Add automatycznie stworzy grupę jeśli takowa nie istnieje. Kod dodaje połączenie skojarzone z klientem, który wywołuje właśnie serwerową metodę.

Powyższy kod jest jednak nie do końca poprawny. Zaglądając do dokumentacji, zobaczymy:

 Task Add(string connectionId, string groupName);

Oznacza to, że jest to metoda asynchroniczna. W praktyce, wywołanie powyższego kodu, zwróci rezultat natychmiast, mimo, że nie ma pewności, że połączenie zostało dodane do konkretnej grupy.

Z tego względu dużo lepiej jest:

public class MyFirstHub : Hub
{
   public async void AddToGroup(string groupName)
   {
       await Groups.Add(Context.ConnectionId, groupName);
   }
   public async void RemoveFromGroup(string groupName)
   {
       await Groups.Remove(Context.ConnectionId, groupName);
   }
}

W ostatnim poście również wspomniałem, że należy dodać następującą referencję:

<script src="~/signalr/hubs"></script>    

Tak naprawdę, jest to referencja do automatycznie wygenerowanego kodu JavaScript. Sprawdźmy, co dokładnie jest wygenerowane:

/*!
 * ASP.NET SignalR JavaScript Library v2.1.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
 *
 */

/// <reference path="..\..\SignalR.Client.JS\Scripts\jquery-1.6.4.js" />
/// <reference path="jquery.signalR.js" />
(function ($, window, undefined) {
    /// <param name="$" type="jQuery" />
    "use strict";

    if (typeof ($.signalR) !== "function") {
        throw new Error("SignalR: SignalR is not loaded. Please ensure jquery.signalR-x.js is referenced before ~/signalr/js.");
    }

    var signalR = $.signalR;

    function makeProxyCallback(hub, callback) {
        return function () {
            // Call the client hub method
            callback.apply(hub, $.makeArray(arguments));
        };
    }

    function registerHubProxies(instance, shouldSubscribe) {
        var key, hub, memberKey, memberValue, subscriptionMethod;

        for (key in instance) {
            if (instance.hasOwnProperty(key)) {
                hub = instance[key];

                if (!(hub.hubName)) {
                    // Not a client hub
                    continue;
                }

                if (shouldSubscribe) {
                    // We want to subscribe to the hub events
                    subscriptionMethod = hub.on;
                } else {
                    // We want to unsubscribe from the hub events
                    subscriptionMethod = hub.off;
                }

                // Loop through all members on the hub and find client hub functions to subscribe/unsubscribe
                for (memberKey in hub.client) {
                    if (hub.client.hasOwnProperty(memberKey)) {
                        memberValue = hub.client[memberKey];

                        if (!$.isFunction(memberValue)) {
                            // Not a client hub function
                            continue;
                        }

                        subscriptionMethod.call(hub, memberKey, makeProxyCallback(hub, memberValue));
                    }
                }
            }
        }
    }

    $.hubConnection.prototype.createHubProxies = function () {
        var proxies = {};
        this.starting(function () {
            // Register the hub proxies as subscribed
            // (instance, shouldSubscribe)
            registerHubProxies(proxies, true);

            this._registerSubscribedHubs();
        }).disconnected(function () {
            // Unsubscribe all hub proxies when we "disconnect".  This is to ensure that we do not re-add functional call backs.
            // (instance, shouldSubscribe)
            registerHubProxies(proxies, false);
        });

        proxies['myFirstHub'] = this.createHubProxy('myFirstHub'); 
        proxies['myFirstHub'].client = { };
        proxies['myFirstHub'].server = {
            addToGroup: function (groupName) {
                return proxies['myFirstHub'].invoke.apply(proxies['myFirstHub'], $.merge(["AddToGroup"], $.makeArray(arguments)));
             },

            removeFromGroup: function (groupName) {
                return proxies['myFirstHub'].invoke.apply(proxies['myFirstHub'], $.merge(["RemoveFromGroup"], $.makeArray(arguments)));
             }
        };

        return proxies;
    };

    signalR.hub = $.hubConnection("/signalr", { useDefaultPath: false });
    $.extend(signalR, signalR.hub.createHubProxies());

}(window.jQuery, window));

Jak widać, plik zawiera głównie proxy. Dzięki niemu, możemy w łatwy sposób w JavaScript wywołać kod C# znajdujący się na serwerze:

 proxies['myFirstHub'].server = {
            addToGroup: function (groupName) {
                return proxies['myFirstHub'].invoke.apply(proxies['myFirstHub'], $.merge(["AddToGroup"], $.makeArray(arguments)));
             },

            removeFromGroup: function (groupName) {
                return proxies['myFirstHub'].invoke.apply(proxies['myFirstHub'], $.merge(["RemoveFromGroup"], $.makeArray(arguments)));
             }
        };

ASP.NET: SignalR i Hub API

W poprzednim poście przedstawiłem framework SignalR oraz niskopoziomową klasę PersistentConnection. Zwykle jest ona trudna w użyciu i dlatego w praktyce dużo częściej korzysta się z Hub API.  Jak sama nazwa wskazuje, stanowi on koncentrator czyli miejsce gdzie składujemy metody zarówno wykonywane po stronie serwera jak i klienta. Dzięki niemu możemy wywołać metodę C# (serwer) jak i JavaScript (klient). Umożliwia on zatem komunikacje obustronną.

Spróbujmy zatem przerobić przykład z poprzedniego postu, aby używał Hub zamiast PersistentConnection. Najpierw oczywiście usuwamy MyFirstConnection i zastępujemy ją hub’em:

public class MyFirstHub : Hub
{
   public void SendMessage(string message)
   {
       Clients.All.newMessage(string.Format("{0}: {1}",DateTime.Now,message));
   }
}

Nasz hub ma jedną metodę teraz – Send Message. Proszę zauważyć, że All jest typu dynamic i dlatego możemy jakąkolwiek metodę wykonać. Metody są definiowane w JavaScript dlatego naturalne jest, że taka metoda nie zostanie automatycznie wykryta przez VS.

Powyższy kod jest implementacją serwerowej metody  SendMessage.  Po odebraniu wiadomości jest ona wysyła do wszystkich klientów (broadcast). newMessage musi zatem zostać zdefiniowana w JavaScript po stronie klienta:

<script type="text/javascript">
    $(function () {

        var myFirstHub = $.connection.myFirstHub;
        myFirstHub.client.newMessage = function (message) {
            $('#messages').append('<li>' + message + '</li>');
        };

        $.connection.hub.start();
        
        $("#broadcast").click(function () {
            myFirstHub.server.sendMessage($('#msg').val());
        });

    });
    </script>

<input type="text" id="msg" />
<input type="button" id="broadcast" value="broadcast" />

<ul id="messages"></ul>

Najpierw tworzymy hub’a a potem za pomocą obiektu server możemy wysyłać wiadomości do serwera. Analogicznie, istnieje możliwość definiowania metod po stronie klienta, które mogą być wykonywane przez serwer.

Należy koniecznie dodać referencje do automatycznie wygenerowanych hubów w JS:

<script src="~/Scripts/jquery-1.10.2.js" type="text/javascript"></script>
<script src="~/Scripts/jquery.signalR-2.1.0.js" type="text/javascript"></script>
<script src="~/signalr/hubs"></script>    

Na zakończenie wystarczy tylko zmapować hub’a do URL:

[assembly: OwinStartupAttribute(typeof(WebApplication9.Startup))]
namespace WebApplication9
{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            ConfigureAuth(app);
            app.MapSignalR();
        }
    }
}

W kolejnej części zajmiemy się bardziej szczegółowo wywoływaniem metod w SignalR.

ASP.NET: SignalR

Aplikacje webowe przeszły rewolucje od czasów pierwszych stron internetowych. Przez długi czas, aplikacje webowe opierały się na prostym schemacie zapytania i odpowiedzi. Wpisując jakiś adres w przeglądarce, generowany był po prostu dokument. Nie było możliwości interakcji. Nawigacja do innej strony, skutkowała przeładowaniem całej strony.

Później pojawił się AJAX, czyli wykonywanie metod usługi w tle. Był to na pewno krok do przodu ponieważ nie było potrzebne już przeładowanie całej strony, aby odświeżyć jakiś tylko fragment. Niestety wciąż była to komunikacja jednokierunkowa, od przeglądarki do serwera. W przeciwnym kierunku byłoby to możliwe wyłącznie poprzez przeładowanie całej strony. Nie istniał wtedy sposób na  łatwe powiadomienie przeglądarki, że stan bazy danych zmienił się.

Możliwe były oczywiście pewne obejścia. Najczęściej przeglądarka odpytywała serwer, czy są już jakieś zmiany, które powinny być odzwierciedlone w przeglądarce. Oczywiście w aplikacjach czasu rzeczywistego nie jest to perfekcyjne rozwiązanie, ponieważ wiąże się z dużym obciążeniem łącza i wynikającymi z tego opóźnieniami.

Dzisiaj na szczęście mamy inne, lepsze rozwiązania. Nie będę opisywał w tym poście szczegółów, ponieważ, chcę najpierw skupić się na samym framework’u SignalR, a potem dopiero prześledzimy jego internale.

W skrócie, tzw. Web Socket API umożliwia pełną komunikację między przeglądarką a serwerem. Możliwe jest zatem dzięki Web Socket, że klient wysyła w tle wiadomość do serwera (nic nadzwyczajnego) oraz serwer do przeglądarki (i to jest nowość). Jest to ogólny standard i nie ma nic wspólnego z ASP.NET. Ponadto, nie jest on wyłącznie ograniczony do przeglądarek internetowych. Działa zwykle na porcie 80 (korzystne dla firewalls itp.).

W HTTML 5 API do dyspozycji są tzw. Serer-Sent Events. Dzięki nim, serwer może powiadomić przeglądarkę o zmianach. Jest to jednak komunikacja również jednokierunkowa, od serwera do klienta.

SignalR jest z kolei frameworkiem dla ASP.NET ułatwiającym pisanie aplikacji czasu rzeczywistego, czyli takich gdzie nie chcemy przeładowywać całej strony bo dane zmieniają się w czasie “rzeczywistym”. Oczywiście kluczowym mechanizmem w takiej scenariuszu jest powiadomienie klientów o zmianach na serwerze. Można oczywiście bezpośrednio korzystać z Web Socket czy HTTML 5 SSE. Problem w tym, że część przeglądarek nie wspiera Web Socket ani SSE. SignalR za nas dostarczy warstwę abstrakcji, czyli wykorzysta najlepszą, dostępną aktualnie technologie.

Jeśli przeglądarka i serwer dostarczają Web Socket, to zostanie on oczywiście wykorzystany bo jest jedynym w pełni dwukierunkowym sposobem komunikacji. W przeciwnym razie HTML 5 SSE  może zostać użyty. W najgorszym przypadku, po prostu stary mechanizm odpytywania zostanie wykonany. Dzięki SignalR, programista ma jeden framework i nie interesuje go, co dokładnie przeglądarka wspiera. Framework dobierze najlepszą technologie jaka jest aktualnie dostępna. Programista  korzysta zawsze z tego samego API.

W SignalR do dyspozycji mamy dwa typy połączeń. PersistentConnection oraz HubAPI. Pierwszy z nic jest bardziej niskopoziomowy i dlatego dzisiaj nim zajmiemy się. W przyszłym wpisie pokażę Hub API.

Zaczynamy oczywiście od instalacji SignalR z NuGet:

image

NuGet zainstaluje pakiet, w którego skład wchodzą m.in. skrypty JS, biblioteki DLL oraz zostanie również wygenerowany Startup:

[assembly: OwinStartupAttribute(typeof(WebApplication9.Startup))]
namespace WebApplication9
{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            ConfigureAuth(app);
        }
    }
}

Na razie o Startup nie musimy martwić się. Stwórzmy nasze pierwsze połączenie:

public class MyFirstConnection:PersistentConnection
{
   protected override Task OnReceived(IRequest request, string connectionId, string data)
   {
       return Connection.Broadcast(data);
   }
}

Przeładowujemy tutaj OnReceived, metodę, która jest wywołana, gdy któryś z klientów wyśle jakąś wiadomość. Następnie tą samą wiadomość, przekazujemy do pozostałych (broadcast).

Pora na kod JS, czyli stronę klienta (przykład z dokumentacji SignalR):

$(function () {
        var connection = $.connection('/echo');

        connection.received(function (data) {
            $('#messages').append('<li>' + data + '</li>');
        });

        connection.start().done(function() {
            $("#broadcast").click(function () {
                connection.send($('#msg').val());
            });
        });

    });

Jak widzimy, mamy tutaj kod, polegający na zewnętrznych bibliotekach np. metoda $.connection. Musimy zatem podłączyć bibliotekę jquery.signalR-2.1.0, która została zainstalowana w ramach pakietu SignalR (NuGet). Oprócz tego, musimy dodać referencje do jQuery:

<script src="~/Scripts/jquery-1.10.2.js"></script>
<script src="~/Scripts/jquery.signalR-2.1.0.js"></script>

Ostatnia rzecz jaka nam pozostała to mapowanie. Powyższy przykład, używa metody echo, która nie jest jeszcze zdefiniowana. Można zobaczyć w $.connection, że odwołujemy się do /echo, ale póki co nie zaimplementowaliśmy tego. Wystarczy, że zmapujemy /echo do naszego połączenia MyFirstConnection we wspomnianej wcześniej klasie Startup:

public partial class Startup
{
   public void Configuration(IAppBuilder app)
   {
       ConfigureAuth(app);
       app.MapSignalR<MyFirstConnection>("/echo");
   }
}

Po uruchomieniu aplikacji, szybko przekonamy się, że wpisując dane w oknie jednej przeglądarki, zostaną one rozesłane do pozostałych, podłączonych klientów (broadcast).

MVVM w JavaScript, knockout.js, część II

W poprzedniej części przedstawiłem bibliotekę knockout.js, ułatwiającą implementację wzorca MVVM w JavaScript. Jest to ciekawa biblioteka, szczególnie dla programistów WPF. W tamtym wpisie, pokazaliśmy jak wiązać dane w dwóch kierunkach: od VM do widoku i odwrotnie. Skończyliśmy na następującym przykładzie:

var personViewModel = {
firstName: ko.observable('poczatkowa wartosc'),
lastName: ko.observable('poczatkowa wartosc') };

ko.applyBindings(personViewModel);

Gdzie widok to:

<p data-bind="text: firstName"></p>
<p data-bind="text: lastName"></p>

<input type="text" data-bind='value: firstName' />
<input type="text" data-bind='value: lastName' />

W WPF bardzo często tworzymy właściwości tylko do odczytu, które zwracają jakieś dane na podstawie innych właściwości. W C# wystarczy stworzyć właściwość wyłącznie z getterem, który używa z kolei jakiś innych właściwości. Oczywiście analogiczny scenariusz możemy zaimplementować w JS. Zaktualizujmy najpierw widok:

<p data-bind="text: firstName"></p>
<p data-bind="text: lastName"></p>
<p data-bind="text: fullName"></p>

<input type="text" data-bind='value: firstName' />
<input type="text" data-bind='value: lastName' />

FullName zatem jest właściwością, która zwraca połączone string’i firstName i LastName:

function PersonViewModel() {
   this.firstName = ko.observable('poczatkowa wartosc');
   this.lastName = ko.observable('poczatkowa wartosc');
   this.fullName = ko.computed(function() { return this.firstName() + ' ' + this.lastName(); }, this);
};

Tak jak to z knockout.js, korzystamy z obiektu ko. W C# wystarczy stworzyć właściwość, niestety w JS nie ma takiej możliwości i należy wywoływać specjalne metody na ko. Proszę zwrócić uwagę, że observable zwraca funkcję, zatem nie jest to zwykłe pole.

Kolejną przydatną rzeczą są komendy (analogiczne do ICommand w WPF). Zacznijmy od widoku:

<button data-bind="click: save">Zapisz</button>

Widzimy tutaj nowy typ wiązania, click. Wcześniej korzystaliśmy z text oraz value.

Następnie musimy zdefiniować handler:

function PersonViewModel() {
   this.firstName = ko.observable('poczatkowa wartosc');
   this.lastName = ko.observable('poczatkowa wartosc');
   this.fullName = ko.computed(function () { return this.firstName() + ' ' + this.lastName(); }, this);
   this.save = function() {
       alert('Save: ' + this.fullName());
   };
};

Możliwe jest również stworzenie czegoś analogicznego do ObservableCollection:

function PersonViewModel() {
   this.firstName = ko.observable('poczatkowa wartosc');
   this.lastName = ko.observable('poczatkowa wartosc');
};

function MainViewModel() {
   this.persons = ko.observableArray();
}

vm = new MainViewModel();

person1 = new PersonViewModel();
person1.firstName('imie1');
person1.lastName('nazwisko');

person2 = new PersonViewModel();
person2.firstName('imie2');
person2.lastName('nazwisko2');

vm.persons.push(person1);
vm.persons.push(person2);

Widok:

<ul data-bind="foreach: persons">
    <li>
        <p data-bind="text: firstName"></p>
        <p data-bind="text: lastName"></p>
    </li>
</ul>

Kolejny typ wiązania to foreach. Służy on oczywiście do powtarzania pewnej części kodu HTML. W WPF jest to nic innego jak ItemsSource oraz ObservableCollection.

Do dyspozycji mamy również zmienną $root, dzięki której możemy wywołać np. komendę z głównego VM:

<ul data-bind="foreach: persons">
    <li>
        <p data-bind="text: firstName"></p>
        <p data-bind="text: lastName"></p>
        <button data-bind="click: $root.save">Zapisz</button>
    </li>
</ul>

Gdzie VM, to:

function MainViewModel() {
   this.persons = ko.observableArray();
   this.save = function() {
       alert('Zapisz');
   };
}

Oczywiście to dopiero początek możliwości knockout. Jeśli kogoś temat zaciekawił to odsyłam do dokumentacji. Na blogu, będę zamieszczał tylko takie krótkie wprowadzania do różnych bibliotek, po to aby zachęcić czytelników, albo po prostu pokazać, że taka możliwość istnieje i została już zaimplementowana. Nie ma sensu bowiem duplikować tego, co jest już w dokumentacji.

MVVM w JavaScript, knockout.js

Od jakiegoś czasu, coraz więcej czasu poświęcam nad aplikacjami webowymi stąd na blogu tematyka trochę zmieniła się z kodu czysto server-side na web. Większość programistów C# nie przepada za JavaScript, ale trzeba robić wszystko, aby ułatwić sobie codzienną pracę. Istnieje wiele dodatkowych bibliotek do JS i to dzięki nim środowisko JS jest tak potężne. Na blogu raczej nie będę zajmował się zbytnio tą tematyką ponieważ nie jest to do końca moja specjalizacja. Czasami jednak, tak jak dzisiaj, wrzucę kilka słów o ciekawych według mnie bibliotekach, które powinny znaleźć się w każdym webowym projekcie. Jako, że sporo korzystałem z WPF, to wzorzec MVVM był dla mnie codziennością. Z tego względu, biblioteka knockout od razu zaciekawiła mnie.

O czystym MVVM pisałem już np. tutaj:

http://www.pzielinski.com/?p=1055

Zdefiniujemy najpierw nasz widok. W przypadku WPF, robiło to się za pomocą XAML oraz dyrektyw {Binding}. Zobaczmy jak to wygląda w HTML i knockout.js:

<p data-bind="text: firstName"></p>
<p data-bind="text: lastName"></p>

Nie wygląda to aż tak brzydko… Korzystamy z atrybutu data-bind a następnie określamy za pomocą text, że chcemy powiązać tekst, który znajduje się w danej właściwości. Oczywiście musimy stworzyć również ViewModel:

var personViewModel =  { firstName: 'Piotr', lastName:'Zielinski' };
ko.applyBindings(personViewModel);

Aby wykonać wiązanie należy wywołać metodę applyBindings na obiekcie ko (knockout.js). Jak widać, nie jest to trudne i dzięki temu oddzielimy logikę od warstwy prezentacji. Korzystając z czystego JS, można napisać naprawdę brzydki kod i projekt. Na szczęście mnogość bibliotek (np. RequireJS) pozwalają na zastosowanie tego języka w bardziej skomplikowanych projektach.

Spróbujmy napisać trochę inny przykład, pokazujący wiązanie w drugą stronę. Innymi słowy, chcemy aby użytkownik mógł modyfikować dane. Typowy scenariusz to oczywiście zwykły formularz. Musimy zmodyfikować nieco nasz ViewModel. W C# zwykle wystarczy dodać SETTER do danej właściwości.

var personViewModel = { 
firstName: ko.observable('poczatkowa wartosc'), 
lastName: ko.observable('poczatkowa wartosc') };

ko.applyBindings(personViewModel);

Musimy skorzystać z metody observable. W WPF zwykle korzysta się z INotifyPropertyChanged oraz OnPropertyChanged. Pełni to analogiczną funkcję – pozwala wykryć, kiedy właściwość została zmieniona. Następnie musimy dodać dwa nowe pola edycyjne:

<p data-bind="text: firstName"></p>
<p data-bind="text: lastName"></p>

<input type="text" data-bind='value: firstName'/>
<input type="text" data-bind='value: lastName'/>

To co rzuca tutaj się w oczy, to nowy typ wiązania, a mianowicie “value”. Korzystamy z niego, gdy daną wartością chcemy zaktualizować właściwość. W WPF, korzysta się z typu wiązana (OneWay, TwoWay itp.), aby osiągnąć analogiczny efekt.

Uruchamiając przykład, zobaczymy, że zmieniając dane w polu edycyjnym, zostaną one również odzwierciedlone w tag’u <p>.

Na dzisiaj wystarczy. W przyszłych wpisach pokażę trochę więcej przykładów. Osobiście uważam, że biblioteka jest bardzo przydatna i naturalna dla programistów, którzy pracują również sporo z WPF.