Other kategória bejegyzései

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.

Delphi Prism – Új kifejezés típusok

A Pascal nyelvtanban a legalapvetőbb komponens ami egy metódus testet alkot, egy statement (“állítás”). Egy metódus test listája a statementeknek, elválasztva pontosvesszővel egymástól őket. x := 5 egy statement, akárcsak a MyObject.DoSomething. A begin/end blokk szintén egy statement, ami többek között saját magába is beágyazható, illetve tartalmazza egy listáját további statementeknek.

Van egy speciális típusa a statementeknek, amik elég gyakran használtak, ezek pedig a kifejezések (expressions). Egy kifejezés egy statement, aminek van egy értéke és így felhasználható bármilyen kód konstrukcióban ami elvár egy értéket, úgy mint jobb oldalán egy hozzárendelésnek, aritmetikai formulákon belül, vagy mint paraméter egy másik metódusra például. Az 5 egy kifejezés, így a MyObject.ToString is.

A korai Pascal nyelvekben (még a Delphi előtt), a kifejezések nem kellett, hogy full statementek legyenek. Például, a függvények (eljárások, amik visszaadnak egy értéket) nem voltak hívhatóak a visszatérési érték használata nélkül. Más szóval, például az sqrt(9) önmagában nem volt egy érvényes statement, de az x := sqrt(9) igen. Ez egy dolga a múltnak.

Funkcionális programozás

Na hova akartam én mindezzel kijukadni? Nos, az eljövő Spring 2010-es kiadásával a Delphi Prism-nek a fejlesztők kiterjesztették a nyelvet. Van három közösen használt statement, melyet innentől fogva mint kifejezéseket fog kezelni a fordító. Ezek a lépések szükségesek voltak ahhoz, hogy több funkcionális programozási koncepció kerülhessen a nyelvbe. A funkcionális programozás egy egészen különböző paradigma, és mint kiderült nagyon jól használható hatékony többszálú kód írásához (lsd. Erlang vagy Haskell). A Microsoft is fokozta a munkát az F#-on, ami a .NET-hez készített első saját funkcionális nyelvük lesz, melyet a VS 2010 is tartalmazni fog többek között.

A Prismhez a fejlesztők egy kissé másfajta megközelítést választottak. Kiterjesztik a nyelvet további funkcionális nyelvi elemekkel, de a cél továbbra is ezek síkban tartása a tradícionális Pascal szintaxissal és érzéssel. Az F#-nak például van néhány érdekes koncepciója ami erőteljessé teszi a nyelvet, de istentelen és közel érthetetlen szintaxis.

A következő új kifejezés típusok az első lépések ebbe az irányba Delphi Prism részről:

“if” kifejezések

Az “if” statement engedi a feltételes futtatását egyik vagy másik statementnek, egy boolean feltételtől függően. Ez gondolom idáig nem új:

if Condition then
  ThenStatement
else
  ElseStatement;

A kulcsszó itt a statement. Mind a then és az opcionális else ág tartalmaz egy statement-et (nem egy kifejezést!). A teljes “if” kikötés egy statement (de egy kifejezésen). A fejlesztők most kiterjesztették ezt a szintaxist, engedve a teljes “if” kikötést kifejezéssé válni:

if Condition then
  ThenExpression
else
  ElseExpression;

A teljes konstrukció egy kifejezés, reprezentálva az értékét a ThenExpression vagy ElseExpression-nek. Ennek következtében például a következő szintaxis a jövőben teljesen érvényes:

var s := 'The Condition is ' + if Condition then 'true' else 'false';

A típusa az “if” kifejezésnek (jelen esetben string) a két kifejezés legkisebb közös nevezője által kerül meghatározásra. Amenyiben az opcionális else ág hiányzik, az eredmény egy false feltételhez nil lesz, illetve ha a then kifejezésnek a típusa egy érték típus, akkor az “if” kifejezés egy nullázható típus lesz. Példa:

var x := if Condition then 3;

Az “x” egy nullázható Int32 típus lesz és tartalmazni fogja a 3 vagy nil értékek egyikét a feltétel teljesülésétől függően.

Egyesek most biztos bekeverik ide a C# conditional operátorát (?:), ami egyébként Delphi Prism-ben szintén van iif néven, de ez nem arról szól, hanem magáról a C# if statementjéről is, ha úgy tetszik. Persze az kérdés, hogy a C#-ot mennyire akarják elvinni funkcionális irányba.

“case” kifejezések

Ugyanez a kiterjesztés vonatkozik a “case” statementekre szimmetrikusan. Egy case statement, ahol minden egyes eset egy kifejezés, önmaga is egy kifejezéssé válik. Mint korábban az “if” kifejezésnél, a típus a tartalmazott kifejezések legkisebb közös nevezője által kerül meghatározásra, illetve ha nincs else statement, akkor nullázható lesz:

var s := case Number of
           1: 'One';
           2: 'Two';
         else
           'Many';
         end;

A lényeg valójában az, hogy a kifejezések csak altípusai a statementeknek, melyeknek van egy értékük. S persze ha jobban belegondolunk a fentiekbe, akkor valóban látszik, hogy semmi ok nincs azt gondolni, hogy a fenti statementeknek/kifejezéseknek nem kellene legyen értékük, ezért is innentől kezdve ezek önmagukban is véve kifejezések a nyelvben.

“for” kifejezések

Az utolsó új típusa a kifejezéseknek a “for” kifejezések:

for each x in SomeSequence yield Expression;

és

for x := StartValue to EndValue yield Expression;

Elsőre ránézésre az tűnhet fel, hogy a do kulcsszó ki lett cserélve egy yield-el. Ez főként azért van így, mert a yield-nek sokkal több értelme van egy kifejezés kontextusában. A do inkább futtatásra utal, míg a yield inkább értékre utal minden ciklus iterációban. Ha ez valakinek ismerősen hangzik a Delphi Prism iterátorainál megszokottakhoz, akkor az nem véletlen. Egy “for” kifejezésnek nagyon hasonló tulajdonságai vannak, illetve iterátori viselkedése, így nevezhetjük őket akár “anonymous iterators”, azaz névtelen iterátoroknak is.

Tehát mi egy értéke egy “for” kifejezésnek? Az értéke egy “for” kifejezésnek egy szekvencia, tartalmazva minden egyes kifejezést. Példa:

var SomeNumbers := sequence of Int32; // IEnumerable<Int32>
SomeNumbers := for i: Int32 := 0 to 100 yield i * i;
// SomeNumbers tartalmazza 0-tól 100-ig az egész számok négyezeteit

Pont mint egy iterátor, ez a szekvencia is on-the-fly lesz generálva, amikor felsorolttá válik. A fenti példában nincs kód ami futna, hogy kalkulálja a számokat 0 –tól 10000-ig. A “for” loop maga egy O(1) művelet. Ez nincs addig, amíg hozzá nem férünk a szekvenciához, például egy másik for each loop használata által. Újra:

var AllInts := for i: Int64 := 0 to Int64.MaxValue yield i;

Ez például egy szekvenciájává válik minden pozitív Int64 értéknek, de a tényleges hozzáférés csak itt következik be:

for each x in AllInts index i do
begin
  if i <= 1000 then break;
  Console.WriteLine(x);
end;

Ezek az új kifejezéstípusok is már részét képezik az új Delphi Prism 2010 “Spring”-nek, ami hamarosan értkezik a Visual Studio 2010-el.