WCF Web API bevezető

Manapság a fejlesztők nap mint nap új kihívásokkal szembesülnek azzal kapcsolatosan, hogyan tegyék közkincsé adataikat, szolgáltatásaikat mások számára. Én például évek óta folyamatosan ezen dilemmák tengerében hánykolódok, és nem véletlenül. Általában amikor neki kezdek egy projekt fejlesztésének, mindig messzemenő távlatokban próbálok gondolkodni, túl a határain a jelenlegi eszközöknek, platformoknak. Mondhatom, hogy egyenlő partnert szeretnék látni minden fejlesztőben, legyen az .NET, Java, PHP, Delphi, C++, Objective-C, Visual Basic, vagy bármilyen fejlesztő.

Én azt szeretném, hogy ezek mindegyike egyformán részesülhessen a munkám gyümölcséből, azaz nyitni szeretnék egy új, szélesebb világ felé, nem pedig ahogy Anders Heijlsberg is fogalmazott, egy fekete dobozba bezárkózni, és ezáltal gyakorlatilag a játszóterem határait súlyosan megszabni, ahol ráadásul magam vagyok saját magam gátja, vagy még drámaiasabban, ahova csak azt engedem be, akit személyesen ismerek és csak azzal a feltétellel, hogy vigye az egész játszóteret, a saját játszóterét pedig égesse fel, mondván: “itt az enyém, az jobb lesz Neked”. Arról már nem is beszélve, hogy melyik a költségtakarékosabb megoldás mindkét fél számára.

Fejlesztői szempontból megvilágítva a problémát: ha van egy dektop, mobil, vagy webes alkalmazásom, ami tud egy kis millió nagyszerű dolgot, üzleti szempontból értékes szolgáltatásokat nyújt, nagy rá a kereslet a piacon (rengeteg van ilyen jelenleg), akkor érdemes elgondolkozni azon, hogy ezt ne csak mint egy kézzel fogható csomagolt terméket (fekete doboz hasonlat), hanem mint szolgáltatást is elérhetővé tegyem mások számára, amiből szintén én profitálhatok, de úgy, hogy közben másokat se kell rákényszerítenem semmi fajta lemondásra, lásd például Facebook API, Twitter API, Google API, Microsoft Live, stb.

Ezzel csak azt akartam érzékeltetni, hogy igenis ideje lenne megnyílni a kisebb, sőt mikrovállalatoknak is affelé, hogy ami értéket teremtenek, azt a teljes világ felé megtegyék egyúttal. Ehhez természetesen hozzátartozik nem pusztán a szolgáltatásban, mint fogalomban való gondolkodás, hanem más infrastruktúrális elvárások is, belevéve a globalizációt és lokalizációt egyaránt, a multiplatform támogatást, stb., stb.

A probléma gyökerei mélyre nyúlnak és nagyon szerteágazóak. Nem pusztán a fejlesztési idő, ill. a költségei nőnek a fejlesztésnek – beleértve a humán erőforrásigényt is, hanem kismillió infrastruktúrális problémával szintúgy meg kell ütközni, amik akár a különböző implementációkból is adódhatnak. Én például régebben még Delphi-s időkben erősen megütköztem a Borland SOAP implementációjával, ami hemzsegett nemcsak a bugoktól, hanem az implementációból fakadó hiányosságoktól is. De akkor most jussunk el odáig, hogy WCF:

A WCF a kezdetektől fogva egy igen erőteljes eszköz, mondhatni  szinte egységesítette az összes kommunikáiós platformot mára. Tényleg fantansztikus látni, hogy a fejlesztőnek csak interfészeket, osztályokat, OOP szemléletű módszertanokat kell alkalmaznia, és minden más alacsonyszintű implementációt elrejt a szeme elől a framework. Az egyetlen nagy probléma idáig az volt vele, hogy az MS teljesen megfeledkezett egy nagyon fontos dologról az évek során, amit úgy hívnak, hogy Web. Miközben a Google erőteljesen átállt a REST alapú megoldásokra és mára mondhatjuk, hogy teljesen elhajította a régi SOAP-os szolgáltatásait, továbbá a web alapú megoldásaival + szolgáltatásaival egy páratlan nagyságrendű piacot hasított ki magának, addig a nagy Microsoft ült és bámult ki a bamba fejéből újjal mutogatva, meg legyingetve egy olyan dologra, amiről már a kezdetek kezdetén tudta mindenki, hogy valami órási nagy dolog van születőben. Ezt még megtetőzte most a Facebook osztatlan sikere is, és végül elértük hála Istennek azt a küszöb értéket, amikor az MS is beszáll egy igen erőteljes eszközzel erre a piacra, ez pedig az új születőben lévő: WCF Web API.

Még mielőtt belemennék a technikai részletekbe, kielemezve, hogy hol tart ez a projekt és merre felé halad tovább, érdemes említést tenni arról, hogy mi volt a probléma a korábbi megoldásokkal. Először is nézzük, miben kellett gondolkoznunk korábban amikor egy WCF szolgáltatáson keresztül valamilyen protokollt (http, https, stb.) szerettük volna publikálni az adatainkat:

Először is, volt az első és legalapvetőbb (személyes eddigi kedvencem), a sima WCF szerviz SOAP endpointon keresztül publikálva. Nagy előnye, hogy teljes uralom van minden felett, beleértve az üzeneteket, a biztonságot, a tranzakciókat, az egyéb rendszerekkel való interoperabilitást, a testreszabhatóságot, stb. További előnye, hogy könnyen fogasztható bármilyen desktop alkalmazásból, ami támogatja az adott verziójú SOAP specifikációt. Általában a fejlesztői eszközöknek van hozzá saját proxy generátoruk, ami szinte egy az egybe odavarázsolta nekünk a szerver implementációt. Sőt .NET kliens proxyk esetében ha a DataContract-ok külön assembly-be vannak fordítva, akkor megoszthatók a kliensel kipipálva ezt az opciót a kliens proxy generálásakor, így a validációs attribútumok és lokalizációs erőforrások is megosztva vannak a kliens és a szerver között, magyarul egy gonddal kevesebb. Egy nagy hátránya, hogy mivel ma már minden a Web-ről szól és szinte minden a böngészőben zajlik, iszonyat körülményes használni kliens oldalon JavaScript-ből és legfőképp, egy kliens oldali JavaScript API-t építeni rá. További hátránya a REST szolgáltatásokkal szemben amit sokan felhoznak még, az automatikus cachelés hiánya. Egyesek úgy látják, hogy ha ők WCF REST vagy WCF Web API szolgáltatásokat készítenek ezt a problémát megoldják, én nem így látom. Tény, hogy ez az előnye megvan az ezen típusú szolgáltatásoknak, de szerveroldalon még mindig szükség van egy átfogó, osztott cache megoldásra (pl. AppFabric), ami garantálja a megfelelő teljesítmény elosztást bevonva akár több gépet is ebbe a folyamatba. Mellesleg statikusan renderelt HTML tartalomra is szükség van az esetek jórészébe, ami SEO szempontjából nem utolsó szempont, és azt nem biztos, hogy a legjobb ötlet REST szervizen átküldeni az ASP.NET MVC alkalmazásba, hogy onnan tovább küldjük a böngésző felé. De jegyzem meg, ilyen fajta támogatás is van a WCF Web API-hoz. Van egy saját fejlesztésű HTTP kliens, ami megkönnyíti az URL mögötti erőforrások (JSON, XML, stb.) deszerializációját bármilyen kliens számára. De ha még fírtatjuk a hátrányokat, amiket lehet sorolni, a WCF-nek van egy szép szolgáltatása, amit úgy hívnak, hogy steaming. Egyértelmű, hogy nagyméretű adatokat, fájlokat ezen keresztül érdemes felküldeni. Egyetlen szépséghibája, hogy egy web app-ból ezt is körülményes használni. Ugyanennek az elérése az új WCF Web API-val gyerekjáték és nincs az az agyalágyult, nyakatekert, kretén megközelítése, mint pl. a WCF Data Service-nek és akkor most térjünk is át rá:

WCF Data Services (Astoria): Mondhatnám, hogy kezdetben vala az Ige, hogy kúrjunk ki mindent úgy ahogy van, mi szem szájnak ingere. Aztán utólag toldozzuk, foldozzuk mi nééékünk nem kölllene, de ha már valami komolyabb dolgot szeretnék, szarjuk szét az egészet, írva 800 sor baromságot a meghackelésére. Röviden és tömören így lehetne összefoglalni ezt a tömör gyönyört iránta. De félretéve a tréfát, célját tekintve ez nem az, ami nekünk kell. Arra kiválóan alkalmas, ha gyorsan össze kell dobni valamit, még JSON formatter is van hozzá, sőt WPF kliensből is gyönyörűen fogyaszható kliens change trackingel, de a Web API paradigmáihoz képest csak egy óvodás szintjével ér fel.

Harmadik, amit aztán tényleg rühellek, az a WCF RIA Services. Na ez az amit az életbe soha nem fogok használni. Ízig vérig rám akarja erőltetni a saját modelljét, sőt még MS platformon belül is gátakat szab, hiszen WPF kliens támogatásnak se híre, se hamva nem volt amikor én ezt utoljára láttam. A legnagyobb baj ezekkel a megoldásokkal, mint pl. a RIA Services, Data Services, hogy leginkább a nyers adatra és azok direkt fogyasztására fektetik a hangsúlyt, mintsem egy rendszerezett, logikailag jól felépített megvalósításra, számba véve az egyéb erőforrások ugyanilyen könnyed módon történő fogyasztását, lásd. fájl feltöltés, letöltés, streaming, stb. esete.

Negyedik a WCF REST, ami mindenre használható, csak web fejlesztésre nem. Ez ugyan már közeledik egy normális REST API-hoz, de még mindig nagyon alacsony szintű és szét kell hackelni a WCF-et egy egyszerű feladat megvalósításához is, mint pl. API kulcs validáció, különböző média formátumok támogatása (JSON, XML, plain/text, vagy amilyet csak akarunk Web API esetében), OAuth 2.0 támogatás, stb. De még idáig el se kell jutnunk, már fejbe vagyunk lőve ott, hogy az MS által implementált fantasztikus DataContractJsonSerializer nem kezeli a körkörös referenciákat, sőt még exception-t is dob amikor a DataContract attributumhoz hozzáírja valaki az IsReference = True tulajdonságértéket, aminek épp az lenne a célja, hogy ezt a prolémát feloldja.

Egy kis kitéről: Hogyan lehet körkörös referenciát gyártani?
Amikor POCO objektumokkal dolgozunk, használva az Entity Framework Code First fantasztikus szolgáltatásait, aminek épp az lenne a lényege, hogy végre modellezhetjük az üzleti elvásároknak megfelelő reprezentációját az adatoknak egy halom DTO osztály gyártása és mappelése helyett, ő szépen betölti az navigációs tulajdonságokon elérhető objektumokat (feltével ha szeretnék, de szeretnék, mert le szeretnénk küldeni), ő szépen azokon is beállítja a referenciákat az alap objektumra. Kész is, megint hackelni kell azt, aminek magától értetődőnek kellene lennie, mert nem lehet JSON-ba szerializálni. Mellesleg itt jegyzem meg, hogy az EF CF-nek Beta állapotban van egy újabb nagyszerű szolgáltatása, melynek neve EF CF Migrations, amely automatizáltan képes követni a változásokat a POCO osztályok kódjaiban és ez alapján upgradelni, sőt downgradelni is képes az adatbázist. Van hozzá C# kódgenerátor is, ami képes ezeket a műveleteket C# kóddal is megírni a hagyományos direkt SQL helyett. Még elég kezdetleges, nálam egy bonyolultabb objektumgráf esetén meghal.

Végre rátérhetünk ténylegesen a WCF Web API-ra, de az már nem ma lesz, mert egy könyvet tudnék írni erről, úgyhogy csak lépésről lépésre. De ha már így említettem a JSON támogatás hiányát (ami természetesen Web API-nál is adott a frameworkbe épített standard MS serializer miatt), és ha szeretné valaki kipróbálni addig is a cuccot, egy kicsit megreformáltam a JSON.NET -hez már mások által készített Media Type Formattert, így most JSONP-t is támogat közkívánatra, csak Nektek:

namespace Microsoft.ApplicationServer.Http
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.Contracts;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Net.Http.Formatting;
    using System.Net.Http.Headers;
    using Newtonsoft.Json;
    using Newtonsoft.Json.Bson;

    public class JsonNetMediaTypeFormatter : MediaTypeFormatter
    {
        private readonly JsonSerializerSettings serializerSettings;

        public JsonNetMediaTypeFormatter()
            : this(new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore })
        {
        }

        public JsonNetMediaTypeFormatter(JsonSerializerSettings serializerSettings)
        {
            Contract.Requires(serializerSettings != null);

            this.serializerSettings = serializerSettings;
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/json"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/bson"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/javascript"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/ecmascript"));
        }

        protected override void OnWriteToStream(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, TransportContext context)
        {
            var serializer = JsonSerializer.Create(this.serializerSettings);

            // NOTE: we don't dispose or close the writer as that would 
            // close the stream, which is used by the rest of the pipeline.
            var writer = GetWriter(contentHeaders, stream);

            var values = default(IEnumerable<string>);
            if (contentHeaders.TryGetValues("jsonp-callback"out values))
            {
                var callback = values.First();
                writer.WriteRaw(callback + "(");
                writer.Flush();
                serializer.Serialize(writer, value);
                writer.WriteRaw(")");
            }
            else
            {
                serializer.Serialize(writer, value);
            }

            writer.Flush();
        }

        protected override object OnReadFromStream(Type type, Stream stream, HttpContentHeaders contentHeaders)
        {
            var serializer = JsonSerializer.Create(this.serializerSettings);
            var reader = GetReader(contentHeaders, stream);
            var result = serializer.Deserialize(reader, type);
            return result;
        }

        private static JsonReader GetReader(HttpContentHeaders contentHeaders, Stream stream)
        {
            var mediaType = contentHeaders.ContentType.MediaType;

            if (mediaType.EndsWith("json") ||
                mediaType.EndsWith("javascript") ||
                mediaType.EndsWith("ecmascript"))
            {
                return new JsonTextReader(new StreamReader(stream));
            }
            else
            {
                return new BsonReader(stream);
            }
        }

        private JsonWriter GetWriter(HttpContentHeaders contentHeaders, Stream stream)
        {
            var mediaType = contentHeaders.ContentType.MediaType;

            if (mediaType.EndsWith("json") ||
                mediaType.EndsWith("javascript") ||
                mediaType.EndsWith("ecmascript"))
            {
                return new JsonTextWriter(new StreamWriter(stream));
            }
            else
            {
                return new BsonWriter(stream);
            }
        }
    }
}

A JSON.NET egyébként egy kitűnő JSON lib, ami támogat camelCase formázást is például, ami szerintem jobban követi a JavaScript standardet, legalábbis ahogy a nagy cégek API-jait nézem, tud ISO dátumba formázni amit pl. az ASP.NET MVC JsonResult nagyon nem szeret megtenni, ezáltal szintén hackelni kell, hogy vissza lehessen postolni adatot scriptből hiba mentesen, használni pedig csak annyi WCF Web API-ból, hogy a Global.asax-ban a WebApiConfiguration példány “Formatters” kollekciójához hozzáadunk egy példányt:

var catalog = new AggregateCatalog(
    new AssemblyCatalog(typeof(Global).Assembly),
    new AssemblyCatalog(typeof(RepositoryBase).Assembly));

var container = new CompositionContainer(catalog);
var config = new MefConfiguration(container);

config.EnableHelpPage = true;
config.EnableTestClient = true;
var settings = new JsonSerializerSettings();
settings.ContractResolver = new CamelCasePropertyNamesContractResolver();
settings.Converters.Add(new IsoDateTimeConverter());
config.Formatters.Insert(0, new JsonNetMediaTypeFormatter(settings));

RouteTable.Routes.SetDefaultHttpConfiguration(config);

Ezt most így kiollóztam, azért olyan amilyen. A MefConfiguration egy WebApiConfiguration származék, csak ezt használtam, mert a Managed Extensibility Frameworkot használom most épp dependency injection-hoz, ami egyébként jóval több ennél, de még akár erre is használható. Ettől függetlenül az MS Unity-vel is szépen megoldható, de mivel úgy látom most az MS a MEF-re szállt rá, akkor legyen.

Szerintem ennyi információ indulásnak elég. Tehát látszik már most kapásból, hogy szépen bővíthető, testreszabható az egész Web API. Saját média típus formatterek, saját HTTP message, exception, response handlerek injektálhatóak bele, stb., stb. Auto formáz xml, json, html, plain text-be. Van már JSONP támogatás, készülőben van az OAuth 2.0 támogatás, sőt már a Facebook OAuth 2.0 támogatás is ott figyel a legfrissebb kódokban, úgyhogy hajrá skacok! Én meg jó szokásomhoz híven hajnali 3-kor ismért ágynak dőlök. Jó éjt! 🙂

jQuery Command Manager Widget II.

Mint korábban ígértem, továbbfejlesztjük az előző kis jQuery widget-et, mégpedig úgy, hogy bármilyen elem bármilyen eseményeihez köthető legyen, sőt javascript kifejezéseket is kitudjunk értékelni. Tehát a cél a következő HTML markup életképessé tétele:

<ul id="menu">
    <li>
        <!-- Egyszerű string command. -->
        <a role="button" href="#" data-command='selectItem'>
            <span>Select</span>
        </a>
    </li>
    <li>
        <!-- A "$value" automatikusan behelyettesítésre kerül a "select" aktuális értével -->
        <!-- ill. megadunk 2 eseményt "click" és "change", amikor eltüzelődjön a command. -->
        <select data-command='{"name":"selectItem","params":"$value","events":"click change"}'>
            <option>Item 1</option>
            <option>Item 2</option>
        </select>
    </li>
    <li>
        <!-- A JavaScript kifejezés kiértékelésre kerül a "select" kontextusába. -->
        <!-- (ha nem adunk meg eseményt, akkor "select"-hez auto a "change" lesz attacholva) -->
        <select data-command='{"name":"selectItem","params":"$(this).val()","eval":true}'>
            <option>Item 1</option>
            <option>Item 2</option>
        </select>
    </li>
</ul>
<script type="text/javascript">

    $(function () {
        $('#menu').commands({
            source: {
                //
                // A "selectItem" command, amit bekötöttünk.
                //
                selectItem: function (options) {
                    alert(JSON.stringify(options));
                }
            }
        })
        //
        // Rögtön elsütjük az első "select" parancsát
        // (a jQuery selector ilyenkor a kontextusban marad).
        //
        .commands('trigger''select[data-command]:first');
    });

</script>

Ahhoz, hogy mindezt életre hívjuk a következőképp kell módosítanunk a korábbi kódunkat:

(function ($, undefined) {

    $.widget('ui.commands', {

        options: {
            source: null
        },

        //
        //  A widget konstruktora, ameny egyszer hívodik meg / DOM element.
        //  Van egy _init() metódus is, a különbség, hogy az mindig eltüzelődik
        //  amikor az adott elemen megpróbáljuk inicializálni többször a widgetet.
        //  Tulajdonképp az is használható lenne a lentebb majd említett parse() helyett.
        //
        _create: function () {

            this._attachEvents();
        },

        //
        //  Hozzáadjuk az eseménykezelőket
        //  a DOM elemekhez, amik "this.element"
        //  kontextusába esnek.
        //
        _attachEvents: function () {

            //
            //  A jQuery widgetek konvencionálisan
            //  "self" elnevezést használnak a widget-re,
            //  ha már szükség van rá egy loopban például.
            //  A loop-on belül pedig $this -t az aktuális elemre.
            //
            var self = this;

            $('[data-command]'this.element).each(function () {
                //
                //  Megrpóbáljuk kihalászni az eseményeket
                //  a data-command-ból. Ha nincs megadva explicit,
                //  akkor mi magunk választunk egy event handler-t
                //  a DOM elem típusa alapján.
                //
                var events =
                    self._parseEventsByElement(this) ||
                    self._createEventsByElement(this);

                if (events) {
                    for (var i = 0; i < events.length; i++) {
                        //
                        //  A widget minden eseményét a ".commands" névtérbe
                        //  kötjük (pl. "click.commands"). A jQuery "bind"
                        //  erre ad lehetőséget nekünk, így együtt tudjuk
                        //  kezelni az összes widget eseményünket amikor unbindoljuk.
                        //  A $.proxy még mindig kell a kontextus átadás miatt.
                        //
                        $(this).bind(
                            events[i] + '.commands',
                            $.proxy(self._commandEvent, self)
                        );
                    }
                }
            });
        },

        //
        //  Eltávolítunk minden ".commands" névteres eseménykezelőt
        //  ami a "this.element" kontextusán belül taláható.
        //
        _detachEvents: function () {

            $('[data-command]'this.element).unbind('.commands');
        },

        //
        //  Parsoljuk az explicit megadott eseményeket, amiket
        //  egy szóköz választ el egymástól.
        //
        _parseEventsByElement: function (element) {

            var events = $(element).data('command').events;
            if (!events) {
                return null;
            }

            return events.split(' ');
        },

        //
        //  Magunk definiálunk eseményeket a megadott DOM elemhez.
        //
        _createEventsByElement: function (element) {

            var events;

            switch (element.nodeName) {
                case 'INPUT':
                    {
                        var type = element.getAttribute('type');
                        events = (type === 'submit' || type === 'button' ? 'click' : 'change');
                        break;
                    }

                case 'SELECT':
                case 'TEXTAREA':
                    {
                        events = 'change';
                        break;
                    }
                default:
                    {
                        events = 'click';
                    }
            }

            return events.split(' ');
        },

        //
        //  Az eseménykezelő, ami a kötött eseményekre eltüzelődik.
        //
        _commandEvent: function (event) {

            event.preventDefault();
            this.trigger(event.currentTarget);
        },

        //
        //  Eltávolít és újra hozzáad minden eseménykezelőt.
        //  A "live" megszűnt, de így orvosolható ha szükséges.
        //
        parse: function () {

            this._detachEvents();
            this._attachEvents();
        },

        //
        //  Eltüzeli a kötött parancsot a megadott elemen.
        //  A "target" nemcsak DOM elem lehet, hanem
        //  lehet akár egy jQuery selector is, a lényeg, hogy
        //  az "this.element" kontextusába működjön.
        //
        trigger: function (target) {

            var element = $(target, this.element);
            if (!element.length) {
                return;
            }

            var command = element.data('command');
            if (!command) {
                return;
            }

            //
            //  Végrehajtjuk az elementen megadott parancsot.
            //  Ha a params === '$value', akkor kinyerjük
            //  az element értékét ( $(...).val() ) és azt passzoljuk
            //  mint paramétert, ha szükség van kiértékelésre (JS expression),
            //  akkor a "target" element kontextusába futtatjuk a kódot.
            //
            this.execute.call(this, {
                name: command.name || command,
                params: command.params ?
                            command.params === '$value' ?
                                element.val() :
                                command.eval ?
                                    function () { return eval(command.params) } .call(target) :
                                    command.params :
                            null
            });
        },

        //
        //  Végrehajtunk egy parancsot, ahol az
        //  options.name a parancs neve, az optons.params
        //  a (kiértékelt) paramétereket tartalmazza.
        //
        execute: function (options) {

            var source, method;
            if ((source = this.options.source) &&
                $.isFunction(method = source[options.name])) {
                options.params ?
                    method.call(source, options.params) :
                    method.call(source);
            }
        },

        //
        //  Felszabadítjuk az erőforrásokat.
        //  Az összes ".commands" eseménykezelőt eltávolítjuk
        //  a kontextuson belül.
        //
        destroy: function () {

            this._detachEvents();

            $.Widget.prototype.destroy.call(this);
        }
    });

})(jQuery);

Ezt még kiegészíthetjük billentyűzet kezeléssel, gyakorlatilag bármivel amit a data-command attribútumban megtudunk adni. A lényegét tekintve a dolognak az, hogy így már egy sokkal rugalmasabb, bármire attacholható command managert kapunk. A command-ok nagy előnye a sima event handlerek írogatásával szemben az, hogy egyrészt nincs szükség gányolásra, szépen elválik a view a logikától, viszonylag könnyen karbantartható, könnyedén tudok egy meglévő parancsot definiálni egy másik elemhez is, mint ahogy azt a fenti példában is láthattuk.

Ez a módszer szépen keverhető a jQuery template és datalink megoldásokkal.

jQuery Command Manager Widget

Már az idejét se tudom mikor írtam utoljára, azóta elmélyedtem egy kicsit az ASP.NET MVC 3, HTML 5, JavaScript, ill. jQuery lelki világába, úgyhogy most inkább ezen a pályán, nem pedig a WPF / Silverlight vonalon mozgok. Megpróbálok bemutatni a jQuery-ből pár hasznos dolgot, főként olyan veteránoknak, akik még most se mozdultak ki a WPF és SL világból.

Talán első körben készítsünk egy saját command manager szerűséget, ami a WPF / SL parancsokhoz hasonlóan közvetíti a kéréseket a kötésbe szereplő forrás objektum és a DOM események között. Ehhez a jQuery widget frameworkot fogjuk használni. Inkább mutatom a scriptet és magyarázom. Alapvetően így néz ki egy nagyon alap widget jQuery-ül kikommentezve:

(function ($, undefined) {

    //
    //  Regisztrálunk egy saját "commands" nevű widget-et.
    //  A jQuery.ui = $.ui az a terület (scope) 
    //  ahol a widget konstruktora tárolásra kerül.
    //  Minden egyéb adminisztratív műveletet 
    //  a widget factory elvégez nekünk.
    //
    $.widget('ui.commands', {

        // 
        //  Egy widget rendelkezik egy inicializációs 
        //  "options" paraméter objektummal. A különlegessége
        //  ennek az "options" objektumnak, hogy automatikusan
        //  kiterjesztésre és felülírásra kerül az egyéni értékekkel
        //  (lásd: jQuery.extend(...) függvény).
        //
        //  Tehát amikor pl. létrehozunk egy ilyen widgetet:
        //      $('#id').commands({ static: false });
        //  felülírjuk az alább megadott default true értéket:
        // 
        options: {
            static: true// Statikus vagy dinamikus kötés (def.: statikus).
            source: null // A forrás objektum (def.: null).
        },

        // 
        //  Ez itt a widget konstruktora, mely automatikusan meghívásra kerül
        //  a widget factory által. Konvenció, hogy a privát tagokat egy
        //  aláhúzással kezdjük: "_create".
        //
        _create: function () {

            //
            //  Itt pl. adhatunk egyéni CSS osztályt a widget
            //  által wrapelt DOM objektumnak, eseménykezelőket
            //  aggathatunk a DOM objektumra ... Magát az DOM elemet a 
            //  "this.element"-en keresztül érjük el.
            //
            this._attachEvents();
        },

        //
        //  Egy saját privát metódus, ami attacholja a szükséges
        //  eseménykezelőket a DOM objektumokhoz.
        //
        _attachEvents: function () {

            //
            //  Ha statikus módot választottunk akkor
            //  minden "data-command" attribútummal
            //  rendelkező DOM elemre, mely a becsomagolt
            //  elem kontextusába esik sima "bind"-al
            //  attacholunk egy click event handler-t.
            //
            if (this.options.static) {
                //
                //  A jQuery selector első paramétere az attribútum szűrés,
                //  második paramétere a kontextus, ami azt a csomópontot jelöli
                //  a DOM-ban, ahonnan a keresés indítandó. Jelen esetben a wrapelt
                //  DOM elemet tekintjük kiindulási pontnak: $('*[data-command]', this.element)
                //  Ezt egyébként mindig érdemes meghatároznunk amikor jQuery
                //  selectorokat használunk, mivel szűkítjük csomópontok számát gyorsabbá téve a keresést.
                //
                //  A jQuery.proxy függvény azt a célt szolgálja, hogy
                //  tetszőleges kontextusba tudjuk futtatni az eseménykezelőt.
                //  Az eseménykezelő neve: "_click", lentebb látható.
                //  Azért működik benne a "this.invoke(...)", mert a "this"
                //  a jQuery.proxy(this, ...) hívás miatt ugyanezt az objektumot jelenti,
                //  nem pedig a klikkelt elemet. De persze az továbbra is elérhető az
                //  event paraméter alatt: event.target, event.currentTarget, stb.,
                //  de így nincs szükség változó elfogásra (variable capture),
                //  ami könnyen vezethet memory leak-hez ha nem bánunk vele körültekintően.
                //
                $('*[data-command]'this.element)
                    .bind('click', $.proxy(this'_click'));
            }
            else {
                //
                //  Ha dinamikus a kötés, akkor használjuk a "live" függvényt.
                //  Annyi a különbség a "bind"-hoz képest, hogy az újonnan,
                //  későbbiekben dinamikusan beszúrt új DOM elemekre is automatikusan
                //  ráaggatja a click event handler-t, de nyilván erőforrásigényesebb.
                //  Ha előre lerendereltettük az egész html tartalmat és nem kezdünk el
                //  gombokat beszúrogatni scriptel, akkor felesleg ez az overhead.
                //
                $('*[data-command]'this.element)
                    .live('click', $.proxy(this'_click'));
            }
        },

        //
        //  Egy saját privát metódus, ami detacholja az összes
        //  eseménykezelőt a DOM objektumokból.
        //
        _detachEvents: function () {

            if (this.options.static) {
                //
                //  Az "unbind" a "bind" párja.
                //
                $('*[data-command]'this.element)
                    .unbind('click'this._click);
            }
            else {
                //
                //  A "die" a "live" párja.
                //
                $('*[data-command]'this.element)
                    .die('click'this._click);
            }
        },

        //
        //  Kezeljük azt az eseményt, amikor a felhasználó egy "data-command"
        //  attribútummal ellátott elemre klikkel és dispécseljük a parancsot
        //  a forrás objektum megfelelő metódusába paraméterekkel együtt.
        //
        _click: function (event) {

            //
            //  Először is megnézzük, hogy az event.currentTarget
            //  rendelkezik-e ezzel az attribútummal.
            //  Megjegyzendő: Nem az "event.target"-et használtam,
            //  mert pl.: <a role="button" href="#"><span>Tovább</span></a>
            //  esetében a span-re való klikkelés a SPAN-t jelenti target-ként.
            //  Ez egyébként nekünk csak jó, mert az esemény buborékozás
            //  miatt együtt tudjuk kezelni a kettőt, de ilyenkor a currentTarget-be
            //  van az aki nekük kell (azért hangsúlyozom, hogy nehogy jQuery.parent()-et hívjunk ezért!):
            //
            var command = $(event.currentTarget).data('command');
            if (!command) {
                return true// Nincs command attrib, mehet Isten hírével amerre lát!
            }

            //
            //  Megakadályozzunk az alapértelmezett esemény eltüzelését.
            //  Ha pl. a href="http://www.devportal.hu" lenne, akkor
            //  ez nem fogja hagyni, hogy elnavigáljunk.
            //
            event.preventDefault();

            //
            //  Ha a parancs egy sima string, akkor azt vesszünk
            //  parancs névként, ha JSON, akkor pedig a name lesz a
            //  parancs neve és a params a paraméterei a parancsnak.
            //
            if (typeof (command) == 'string') {
                this.invoke(
                    command // A neve a parancsnak.
                );
            }
            else {
                this.invoke(
                    command.name, // A neve a parancsnak.
                    command.params, // A paraméterei a parancsnak.
                    command.eval // Kezelje a paramétert mint JS kifejezést.
                );
            }

            return false;
        },

        //
        //  Ha érvénytelen a parancs neve.
        //
        _invalidCommand: function (command) {

            //
            //  Ide jöhet valami egyéni hibakezelő megoldás,
            //  kivétel dobás, jQuery dialógus ablak, logolás, stb.
            //
            throw new Error("Invalid command: " + command);
        },

        //
        //  Itt pedig következik ami diszpécseli a parancsot
        //  és a paramétert a forrás objektumba. Látható, itt már
        //  nincs aláhúzás, úgyhogy ez "public", manulisan is meghívható.
        //
        invoke: function (command, params, eval) {

            if (!command) {
                this._invalidCommand(command);
                return this;
            }

            //
            //  Megnézzük, hogy a forrás objektumon van-e
            //  ilyen tag és ha van, akkor az ténylegesen
            //  egy függvény ($.isFunction(...))
            //
            var source = this.options.source;
            var func = source[command];
            if (!$.isFunction(func)) {
                this._invalidCommand(command);
                return this;
            }

            //
            //  Meghívjuk a függvényt átpasszolva a paramétereket ha vannak,
            //  viszont a "this"-t, azaz a kontextust itt is rendeznünk kell,
            //  hogy a hívott függvényen belül a forrás objektumot lássa
            //  az abba lévő kód a "this" kulcsszón keresztül, amit pedig
            //  a javascript függvény prototípuson definiált call(...)-al tudunk átpasszolni.
            //
            if (params) {
                func.call(source, eval ? window.eval(params) : params);
            }
            else {
                func.call(source);
            }

            //
            //  Ahhoz, hogy láncolhatóak
            //  legyenek a hívások (nem kötelező).
            //
            return this;
        },

        //
        //  A widget destruktora.
        //  Ide kerülnek az erőforrások felszabadításával kapcsolatos kódok.
        //
        destroy: function () {

            //
            //  Detacholjuk az eseményekezelőket, illetve
            //  meghívjuk az "örökölt" destruktort, ami
            //  auto eltávolít minden extra hozzáadott információt a DOM-ból
            //  amit a factory hozzátett. Nyilván a csomagolt elemet nem távolítja el.
            //  Ide jöhetnek például még a CSS osztályok eltávolításaival kapcsolatos dolgok is.
            //
            this._detachEvents();

            $.Widget.prototype.destroy.call(this);
        }
    });

})(jQuery);

Használni pedig már nagyon egyszerű:

Először is kellenek hozzá a jquery.js, ill. a jquery.ui.js fájlok.
A fenti kódot pedig elhelyezhetjük egy külön .js fájlba.
A html fájlba például írhatjuk:

<ul id="context">
    <li><a role="button" href="#" data-command='edit'></li>
    <li><a role="button" href="#" data-command='{"name":"copy","params":{"count":8}}'></li>
    <li><a role="button" href="#" data-command='{"name":"expression","params":"Math.sqrt(16)","eval":true}'></li> </ul>
<script type="text/javascript">
//<![CDATA[
    $(function () {
        $('#context').commands({
            source: {
                edit: function () { },
                copy: function (options) {
                    for (var i = 0; i < options.count; i++) ...
                }
            }
        });
    });
//]]>
</script>

Mint látható, a root jelen esetben a ‘context’ id-el ellátott DOM element lesz, ezen belül pedig ráakaszkodunk az összes létező olyan elemre, aminek van ‘data-command’ attribútuma és az abban található paramétereknek megfelelően hozzákötjük a ‘source’ objektumunk megfelelő metódusaihoz. Mivel az ‘invoke’ nem privát, ezért az inicializáció után meghívhatjuk az egyes parancsokat pl. így is: $(‘#context’).commands(‘invoke’, ‘edit’); A destruktor ugyanígy hívható: $(‘#context’).commands(‘destroy’); és akkor detacholunk minden ‘_click’ event handlert a root elemen belül.

Később még tovább fejlesztjük ezt, ill. csinálunk ettől komolyabb dolgokat is, most csak illusztrálni szerettem volna egy példán keresztül, hogy nem az ördögtől való ez az egész, könnyedén összetudunk rakni magunk is akár olyan sikertörténettel bíró felhasználói felületeket, mint pl. a Facebook, és nem is kell nagyon megerőltetni magunkat. Én már például komplett Google mintán alapuló űrlap szerkesztőn keresztül, komplett css szerkesztőn, webszerkesztőn keresztül, facebook dropdrown gombokon keresztül, URL hashen alapuló státuszkezelésen keresztül (lsd. Facebook), ASP.NET MVC ajax hívás támogatáson keresztül, listák, lapozáson keresztül, minden szörnyűséget megírtam könnyedén jQuery segítségével magamnak.

Ui.: A fenti kódot kiegészítettem még egy eval-el, így a paraméterek kiértékelhetők, mint JavaScript kifejezések akár. Példa:

$('<div id="context"></div>')
.append(
    $('<a role="button" href="#" data-command=\'{"name":"edit","params":"Math.sqrt(16)","eval":true}\'></a>')
)
.commands({
    source: {
        edit: function (options) {
            alert(options);
        }
    }
})
.children('a[data-command]')
.trigger('click');

MVVM (WPF & Silverlight) Beta 2

Néhány változás:

Az EditStateChanged esemény most közli a módosított tulajdonságokat is, esetleg ha szükség lenne loggoláshoz, vagy bármihez. Ez akkor váltódik ki amikor az IEditableObject műveletek használtak akár az EditableObject, akár az EditableViewModelBase származékok esetében.

Van egy új PagingHelper osztály a Helpers mappába, illetve ehhez egy extension az Extensions mappába. Így könnyebb lapozható query-ket írni.

Service

Az egyik legérdekesebb része a dolognak. Mivel egy-egy művelet lehet komplexebb, több input és több outputal, így ezt elvonatkoztatva a konkrét példáktól, generikusabb módszert választottam. Szolgáltatás esetében most tisztázzuk, hogy nem WCF szolgáltatásról, vagy hasonlóról beszélünk, hanem tulajdonképpen a model, mint szolgáltatás rétegről van szó. A történet egyszerű, szolgáltatás műveletek vannak, melyeket interfészek írnak le, pontosan két típus:

Ha nincs visszatérési érték:
interface IServiceAction<in TRequest> : IServiceOperation<TRequest>

Ha kapunk vissza értéket:
interface IServiceFunc<in TRequest, TResponse> : IServiceOperation<TRequest>

Mindegyikhez tartozik egy alap implementáció, melyek neve:

class ServiceActionBase<TService, TRequest> : ServiceOperationBase<TService, TRequest>, IServiceAction<TRequest>

class ServiceFuncBase<TService, TRequest, TResponse> : ServiceOperationBase<TService, TRequest>, IServiceFunc<TRequest, TResponse>

Az implementáció tartalmaz egy szinkronizációs kontextust (ami WPF és SL esetében a dispatcher kontextus lesz alapértelmezésben), illetve magát a tényleges szolgáltatást (TService), ami lehet egy WCF Data Service kontextus, sima WCF szolgáltatás, vagy akár egy generikus lista (List<IPerson>) is teszteléshez, mint adatforrás (szolgáltatás).

Minden szervíz művelet meghatároz egy abstract Execute metódust, aminek a Request lesz a paramétere. Amikor saját műveletet írunk, ezt kell mindig felülírnunk. Mutatok egy példa implementációt WCF Data Service-hez:

Ugye nem akarunk 667 query-t, meg anyám kínját írni minden lapozáshoz, stb. Ezért származtatunk egy service operation-t. Jelenleg én csináltam egy ItemsRequest és egy ItemsResponse osztályt lekérdezésekhez. A ItemsRequest osztály tartalmaz a lapozáshoz szükséges paraméterek megadását, az ItemsResponse pedig az eredményt, illetve az összes tétel számát. Ezalapján csináljunk egy közös őst lekérdezésekhez általában, ami ráadásul a megfelelő felüleletbe is már automatikusan kasztol (Person => IPerson). Ezt a példát megtaláljátok a Tests mappában:

public abstract class DataServiceQueryOperation<TItem, TInterfaceItem>    : ServiceFuncBase<ShopContext, ItemsRequest, ItemsResponse<TInterfaceItem>>{    public DataServiceQueryOperation(ShopContext service)        : base(service) { }

    protected abstract DataServiceQuery<TItem> OnExecute(ItemsRequest request);

    public override void Execute(ItemsRequest request)    {        try        {            var query = (DataServiceQuery<TItem>)(OnExecute(request)                .AddQueryOption("$inlinecount", "allpages")                .Paging(request.Paging.PageIndex, request.Paging.PageSize));

            query.BeginExecute((IAsyncResult ar) =>            {                var list = default(IEnumerable<TInterfaceItem>);                var error = default(Exception);                var response = default(QueryOperationResponse<TItem>);                try                {                    response = (QueryOperationResponse<TItem>)query.EndExecute(ar);                    list = response.Cast<TInterfaceItem>().ToArray();                }                catch (Exception ex)                {                    error = ex;                }

                Synchronization.Post(state =>                {                    OnCompleted(new ResponseEventArgs<ItemsResponse<TInterfaceItem>>(                        error, new ItemsResponse<TInterfaceItem>(list, response.TotalCount)));                }, null);

            }, null);        }        catch (Exception ex)        {            Synchronization.Post(state =>            {                OnCompleted(new ResponseEventArgs<ItemsResponse<TInterfaceItem>>(ex, null));            }, null);        }    }}

Mint látható ez a kis osztály segít egy query-t aszinkron futtatni, majd az eredményt a dispatcher kontextusába (vagy az éppen aktuális szinkronizációs kontextusba) vissza postolni. Az $inlinecount segít, hogy egyetlen kéréssel kapjuk vissza az összes tétel számát is egyúttal. Ez alapján egy Person listát lekérdező művelet:

public class PersonQueryOperation : DataServiceQueryOperation<Person, IPerson>{    public PersonQueryOperation(ShopContext service)        : base(service)    {    }            protected override DataServiceQuery<Person> OnExecute(ItemsRequest request)    {        return Service.Person;    }}

Jó nem? Természetesen csinálhatunk saját request és response-t is, a lényeg ugyanaz. Egységes módon, műveleteket definiálunk a szolgáltatás modellünkbe, kicsit SOA mintára, amiket különböző adatforrásokhoz implementálhatunk. Maga a szolgáltatás csak a műveletet írja le:

public interface IPersonService{    IServiceFunc<ItemsRequest, ItemsResponse<IPerson>> GerPersonList { get; }}

Azt már különböző implementációk döntik el, honnan jönnek ehhez az adatok. További részletekért nézzétek át a teszt alkalmazást. Megjegyzem az a teszt nem teljes, csak azon kísérletezgetek.

ViewModels

Összevontam néhány dolgot, csak egy RepositoryViewModelBase létezik, viszont van egy új property: Pager. A Pager egy RepositoryPagerViewModel, amely az összes szükséges lapozási szolgáltatás commandokba foglalva a felületen elérhetővé teszi. Fontos látni, ez az implementáció nem függ a IPagedCollectionView-tól, amúgy is az WPF-ben nincs is, hanem bármihez jó. A tesztbe szintén láthattok rá példát, hogyan kell XAML-be a felületbe bekötni. DataPager nem támogatott és nem is kell! A lényeg, hogy kezeli a szerkesztést, stb. Így pl. nem lehet addig lapozni, amíg valami szerkesztési állapotba van, illetve auto frissíti a lapozó parancsokat, gombokat. Lehet állítani az oldal méretet is a felületbe kötve, stb. Alapértelmezésbe felülírhatók az OnPageChanging és OnPageChanged tagok, így akár egyénileg megkadályozható a lapozás. A commandok auto kezelik ezt és a UI szintén frissül eszerint. De lehetőség van az egész Pager cseréjére is, felülírva a virtuális property-t. Egy dologra kell figyelni ha saját pager-t csinálunk, hogy az OnPageChanging és OnPageChanged eseményekre iraktkoztassuk fel (nem kötelező, de pl. az OnPageChanged auto meghívja a Refresh metódust, ami újra lekéri az adatokat a PageIndex, PageSize, ItemCount változáskor).

Jó szórakozást hozzá:

VVMF Design Patterns & Practices – Beta 2.

MVVM (WPF & Silverlight) Beta 1

Felraktam az első Beta verziót mind WPF 4.0, mind Silverlighth 4.0-hoz. Mit tud?

Ha belenéz valaki, láthatja hogy van benne minden a világon. Vannak olyan dolgok, amik a Prism implementációból, vannak amik az MS MVVM Toolkit-ből, illetve vannak amik az MVVM Light Toolkit-ből származnak, javarészt kibővítve azok működését. Az alap objektum mindenhez az ObservableObject. Ez implementálja az INotifyPropertyChanging, illetve INotifyPropertyChanged felületeket. SL-be is csináltam INotifyPropertyChanging-et. Aztán van egy EditableObject, ami implementálja az IEditableObject,, INotifyDataErrorInfo, IDataErrorInfo interfészeket, így mind WPF, mind SL-be működik a validáció is. A validáció akárcsak a RIA Service entitás osztályai, a DataAnnotation attribútumokat használja (pl. [Key], [Required], [StringLength]). Jegyzem meg, hogy van egy EditSession osztály is, ami bármilyen osztályhoz újrahasználható az IEditableObject implementálásához. Az EditableObject is ezt használja belsőleg. Az EditSession a chikk által készített Accessor API-t használja és cachel, elkerülve a reflection-t, lásd: Reflection/PropertyOrFieldAccessorRepository.cs. Van egy DispatcherSync statikus osztály, amivel könnyedén tudunk szinkronizálni a Dispatcherrel. Pl. ViewModel-be tanácsos a kollekció műveleteket mindig szinkronizálni a Dispatcher-el, ahol a SafeBeginInvoke() segít annak eldöntésében, hogy szükség van-e rá, vagy sem. Na ennyit az alap osztályokról, térjünk az MVVM részre:

Commands

DelegateCommand
ICommand implementáció paraméter nélkül (pontosabban null paraméterrel).

DelegateCommand<T>
ICommand implementáció egy T típusú paraméterrel.

EventCommand
UI események (event handlers) ICommand-ba diszpécselése.

CommandReference
Segít abban, hogy egy command-ot összeköthessünk valamilyen KeyBinding-al.

WeakCommandManager
Silverlighthoz készítettem, ami megoldja a parancsfrissítési limitációt. Az SL-be a CanExecute kiértékelése nem történik meg automatikusan javarészt, így szükséges meghívni az InvalidateRequerySuggested()-t metódust, ami az összes nyilvántartott CanExecuteChanged handler-t megívja. Most ezt a ViewModelBase minden view model property változáskor automatikusan meghívja, így frissül a UI korrekten, akárcsak WPF-ben. Természetesen a WPF-es változatban ez a CommandManager ugyanezen metódusát hívja meg.

Service

IQueryableService
Leír egy alap lekérdezhető szolgáltatást amiből kitudunk nyerni adatokat. Ennek mindössze egy Load metódusa van és egy esemény, ami akkor váltódik ki, ha a betöltés megtörtént vagy elbukott valamilyen okból kifolyólag. Az OperationEventArgs Error tulajdonsága hordozza ilyenkor a kapott kivételt és a Result az eredmény kollekciót. A lényege ennek a résznek az MVVM-ben, hogy az adatkezelést egy jól szeparált módon valósítja meg, beleértve azt is, hogy teljesen függetleníthető az adatkezelés az adatforrás típusától (lehet az EF, WCF Data Service, WCF RIA Service, WCF, stb.). Ha pl. közös felületeket használunk (IPerson, IOrganization, stb.) – amiket agyrém kézzel megírni mindenhez – univerzálisan rugalmas implementációt kapunk.

IUpdateableService
Ugyanaz mint az előző, csak kiteszi CUD műveleteket, illetve van egy Save, Cancel tagja is. Nyilván nem mindenhol használatos minden, ezért ha nincs felülírva valami NotSupportedException-t kapunk. Ha pl. WCF Data Service-t, vagy RIA Service-t használunk, akkor a kontextus change tracking dolgozik, így a Save használható, sőt RIA-nál a Cancel is.

View Models

ViewModelBase
Hasonló az MVVM Light Toolkit-hez, lényegében a különbség mindössze, hogy van INotifyPropertyChanging (SL-be is), van egy Execute metódus parancsok biztonságos futtatására (tehát a CanExecute-ot is kiértékeli), illetve minden ViewModel és ObservableObject szerializálható is egyben. Az üzenetes megoldás ugyanaz mint a Light Toolkit-ben, van egy singleton Messenger instance, stb.

EditableViewModelBase
Mivel a view model általában kiteszi az alatta lévő model egyes tulajdonságait, s mivel a UI közvetlen ezekkel fog érintkezni, szükséges ezen a szinten is biztosítani a validációt és szerkeszthetőséget. Azért mondom, hogy ezen a szinten is, mert kinek hogy tetszik, van aki szereti a model osztályokban tartani (lásd WCF RIA entitások), valaki a View Model-be felhozza. Ez a scenariotól függ, hogy model osztályokkal dolgozunk, vagy view model wrapperekkel a modellek körül (ez utóbbi a rugalmasabb, de több meló kivezetni a property-ket). De visszatérve, összességében véve ez osztály ugyanaz, mint az EditableObject, viszont ez szerkesztési parancsokat is kitesz már (BeginEditCommand, CancelEditCommand, EndEditCommand), melyek már konyhakészen köthetőek gombokhoz, vagy bármihez ami támogatja a command kezelést.

Fontos megjegyezni, mindent code contract-ok írnak le, így mindenki aki megvalósítja az adott interfészeket, örökli ezeket a kényszerített megszorításokat. Így pl. minden művelet (Open, Close, stb.) csak akkor tud lefutni, ha a hozzájuk tartozó (CanOpen, CanClose, stb.) igaz értéket ad vissza. A compiler auto beilleszti az osztályok metódusaiba az interfész contract megszorításokat, így nincs szükséges egyéni exception mutatványokra az osztályok felületek által leírt tagjaiban.

QueryableRepositoryViewModelBase
Ez tulajdonképp felfogható egy lekérdezés eredmény-kollekciójának. Tartalmazhat view model (mint model wrapper), de akár sima model osztályokat is. A szokásos adatforrás műveleteket implementálja, mint Open, Close, Refresh, illetve nyilvántartja a saját állapotát (State). A UI-ba szinte mindig a CollectionView propertyhez kell kötnünk. A CollectionView előnye, hogy cserélhető (lehet lapozható, szűrhető, csoportosítható, stb.), illetve állapotinformációkkal lát el egy Collection-t. Bár a neve egy nézetre utal, hiszen az is, a kollekció egy nézete, de semmilyen köze nincs olyan vonatkozásban a view-okhoz. Tulajdonképp egy adatforrásnak fogható fel, a WCF RIA is implementál egy saját lapozható Collection View-ot és annak változása során állítja elő a megfelelő lekérdezéseket. A CollectionView CurrentItem tulajdonsága jelöli az aktuális tételt, amit a WPF és Silverlight kötési motorja automatikusan frissít. Ez ki lett vezetve SelectedItem néven. Van IsSynchronizedWithCurrentItem, illetve SynchronizeSelectedItems behavior (SL: ListBox-hoz, WPF: MultiSelector származékokhoz) is a csomagban. A view model SelectedItems tulajdonsága van fenntartva a több tételes kijelölés szinkronizálására.

UpdateableRepositoryViewModelBase
Ugyanaz mint az előző, csak kiegészíti néhány művelettel, mint Insert, Delete, Save, Cancel. Ami nincs implementálva NotSupportedException-t fog dobni, ha használjuk.

Na ennyit így hajnali 4-kor erről. A tesztalkalmazást ne vegyétek alapul, azt csak úgy összeraktam kutyafuttában, de idáig megy, az a lényeg. Itt a kód:

VVMF MVVM Design Patterns & Practices

Kurzor váltogatás

Nem tudom ti hogy vagytok vele, de ha mégis valami szinkron műveletet kell végrehajtani, ami nem túl gyakori WPF esetén, de ha mégis, én unom ezt a try..finally blokkozást. Ráadásul nekem zavarja a szememet is. Ez ugyan némi overheadet ad a dologhoz, de ezerszer áttekinthetőbb és jobb nekem így:

using System;
using System.Windows.Input;

namespace NetFx.Core.WPF.Utilities
{
    public sealed class CursorRegion : IDisposable
    {
        #region Constructors

        public CursorRegion(Cursor cursor)
        {
            Mouse.OverrideCursor = cursor;
        } 

        #endregion

        #region Properties

        public static CursorRegion Wait
        {
            get { return new CursorRegion(Cursors.Wait); }
        } 

        #endregion

        #region IDisposable

        public void Dispose()
        {
            Mouse.OverrideCursor = null;
        }

        #endregion
    }
}

Nem nagy szám, csak jobb életérzést nyújthat, meg több erőforrást eszik. De igaz a mondás, mindig az a finom, ami egészségtelen. 😀 Ennyi használni:

using (CursorRegion.Wait)
{
}

WCF Data Service Kliens Szerializáció

Van egy kis probléma. Ha EF-et használunk, akkor a WCF Data Service a kliensen minden partial class publikus tulajdonságot amit a generált osztályokhoz megadunk szerializálni fog. Az ok az, hogy EF kontextus esetén a WCF Data Service nem használ reflection-t, emiatt nincs is hozzá olyan attribútum ami ezt megkerülhetné. Miért probléma ez? Csak azért, mert a SaveChanges hanyatvágódik tőle és kissé nehézkes a szokványos felületeket, mint IEditableObject, IDataErrorInfo, stb. implementálni publikus tulajdonságok nélkül, amiket pl. WPF és Silverlight vezérlőkhöz is kötni lehet. Szerencsére lehetőség van beleavatkozni abba, hogy mi menjen ki a szerverre, csak egy kicsit meg kell hekkelni a cuccot. Ehhez csináltam egy két segédosztályt. Először kell egy attribútum amit a kliens osztály tulajdonságaira lehet aggatni:

namespace System.Data.Services.Common
{
    [AttributeUsage(AttributeTargets.Property)]
    public class NonSerializedDataAttribute : Attribute
    {
    }
}

Ez eddig okés. Itt jön a lényeg, ráakaszkodunk a data service WritingEntity eseményére:

using System.Collections.Generic;
using System.Data.Services.Client;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Xml.Linq;

namespace System.Data.Services.Common
{
    public class DataServiceContextSerialization
    {
        #region Constructors
        
        public DataServiceContextSerialization(DataServiceContext context)
        {
            Contract.Assert(context != null);
            NonSerializedPropertyCache = new Dictionary<Type, IList<string>>();
            context.WritingEntity += new EventHandler<ReadingWritingEntityEventArgs>(Context_WritingEntity);
        }
        
        #endregion

        #region Properties

        private readonly Dictionary<Type, IList<string>> NonSerializedPropertyCache;

        #endregion

        #region Methods

        private IEnumerable<string> GetNonSerializedProperties(Type type)
        {
            var properties = (IList<string>)null;
            if (!NonSerializedPropertyCache.TryGetValue(type, out properties))
            {
                properties = new List<string>();
                foreach (var property in type.GetProperties())
                {
                    var attributes = property.GetCustomAttributes(typeof(NonSerializedDataAttribute), false);
                    if (attributes.Length > 0)
                    {
                        properties.Add(property.Name);
                    }
                }

                NonSerializedPropertyCache.Add(type, properties);
            }

            return properties;
        }

        #endregion

        #region Event Handlers

        private void Context_WritingEntity(object sender, ReadingWritingEntityEventArgs e)
        {
            var xnEntityProperties = XName.Get("properties", e.Data.GetNamespaceOfPrefix("m").NamespaceName);
            var xePayload = (XElement)null;
            
            foreach (var property in GetNonSerializedProperties(e.Entity.GetType()))
            {
                if (xePayload == null)
                {
                    xePayload = e.Data.Descendants().Where<XElement>(
                        xe => xe.Name == xnEntityProperties).First<XElement>();
                }

                var xnProperty = XName.Get(property, e.Data.GetNamespaceOfPrefix("d").NamespaceName);
                var xeRemoveThisProperty = xePayload
                    .Descendants()
                    .Where<XElement>(xe => xe.Name == xnProperty)
                    .FirstOrDefault<XElement>();

                if (xeRemoveThisProperty != null)
{
xeRemoveThisProperty.Remove();
} } } #endregion } }

A használat egyszerű. Kell egy partial class a saját Data Service kliens kontextushoz egy kidekorált konstruktorral:

using System;
using System.Data.Services.Common;

namespace NetFx.Partnerinfo.ServiceClient.PartnerinfoDataService
{
    public partial class PartnerinfoContext
    {
        public PartnerinfoContext(Uri dataServiceUri, bool customSerialization)
            : this(dataServiceUri)
        {
            if (customSerialization)
            {
                this.CustomSerialization = new DataServiceContextSerialization(this);
            }
        }

        private readonly DataServiceContextSerialization CustomSerialization;
    }
}

Innentől kezdve pedig bármivel ki lehet bővíteni a kliens generált osztályokat, mint pl.:

using System.ComponentModel;
using System.Data.Services.Common;
using NetFx.Core;

namespace NetFx.Partnerinfo.ServiceClient.PartnerinfoDataService
{
    public partial class Address : IEditableObject
    {
        #region Constructors
        
        public Address()
        {
            this.EditableObjectAdapter = new EditableObjectAdapter<Address>(this);
        } 

        #endregion

        #region Editable Adapter

        [NonSerializedData]
        public EditableObjectAdapter<Address> EditableObjectAdapter { get; private set; }        

        #endregion

        #region IEditableObject

        public void BeginEdit()
        {
            EditableObjectAdapter.BeginEdit();
        }

        public void CancelEdit()
        {
            EditableObjectAdapter.CancelEdit();            
        }

        public void EndEdit()
        {
            EditableObjectAdapter.EndEdit();
        }

        #endregion
    }
}

Egy dologra azért figyelni kell így is, setter kell legyen minden tulajdonsághoz. Ha tudtok jobbat és egyszerűbbet, akkor szívesen veszem! 😀

WCF Data Services – Projekciók és DTO-k

Gondoltam kipróbálom már, de már az elején gondjaim adódtak. Világosítson már fel valaki, hogy a fenébe lehet saját típusú objektumokat átvinni ezen?

Én egy módot találtam, de az valami gyönyörű: http://samuelmueller.com/2009/11/working-with-projections-and-dtos-in-wcf-data-services.

MSDN-en is: http://social.msdn.microsoft.com/Forums/en/dataservices/thread/f656ab68-b034-4dc0-a410-02a8c79f1963

Gyártsak komplex típusokat az EF modellben és a lekérdezés eredményéből anonym típusokat, majd újra végigiterálva hozzam létre a megfelelő objekumokat átmásolva a tulajdonságokat. Ez nekem olyan érdekesnek tűnik. 😀

Osztálykönyvtár változások (ObservableCollection, ReadOnlyObservableCollection)

A 4.0 végre nagy változásokat fog hozni minden téren. Abszolút helyére kerülnek a dolgok, és nemcsak magas technológiai szinten, hanem az osztálykönytárban is. Pl. az ObservableCollection WPF mentessé vált, átkerült a System.dll System.Collections.ObjectModel névtérbe a WindowsBase.dll-ből, mintahogy sok más dolog is. Van mellette egyébként egy ReadOnlyObservableCollection is. A lényeg, hogy ami lehet az WPF mentessé válik. Jelen esetben ez pl. azért volt fontos, mert az MS által gyártott WPF MVVM pattern library kénytelen volt pontosan a WPF függőség miatt egy saját NotificationCollection-t implementálni. Ráadásul nem tudom, hogy ti érzitek-e, de bitang gyors lett minden. Szó szerint minden, na meg kisebb is. Ez a 4.0 ott lesz nagyon.