Daily Archives: 2009.10.4. 17:50

ASP.NET AJAX 4: Kliens oldali adatkötés – Sys.Observer

A cikksorozat előző részeiben ([1], [2]) az egyszeri adatkötéssel ismerkedtünk, ami sok esetben nagyon hasznos, de mégis az az igazi, ami a megváltozott adatot automatikusan képes frissíteni a felhasználói felületen. Ehhez arra van szükségünk, hogy észrevegyük, ha egy objektum valamelyik tulajdonsága megváltozik, ami lássuk be, JavaScriptben nem is olyan egyszerű feladat.

JavaScriptben ugyanis nincsenek property-k, legalábbis nem a .NET-es értelemben, azaz nincs lehetőségünk getter és setter metódusok írására. Ugyan van egy javasolt elnevezési konvenció – amit egyébként a Microsoft AJAX Library osztályai követnek is – miszerint írjunk get_ és set_ kezdetű metódusneveket, de ez nagyon körülményes megoldás lenne olyan esetekben, amikor primitív adatstruktúrákkal dolgozunk. Képzeljünk el egy webszolgáltatástól JSONban visszakapott Person objektumokból álló tömböt, hol van abban get_ és set_ metódus? Sehol.

Hasonló problémákra az elterjedt megoldás az observer tervezési minta használata: adott egy publisher, akinek a birtokában van az adat és ő szól az érdeklődő subscribereknek, ha az adat megváltozott. Egy JSONos objektum tömb valóban tartalmazza az adatot, csak éppen nem képes szólni, ha az megváltozik. Természetesen nincs akadálya, hogy felruházzuk a tömbünket ilyen funkciókkal, de ha ezt minden esetben nekünk kell megírnunk, akkor az egyrészt nagyon sok munka (JavaScriptben a sima “sok”=”brutál sok”), másrészt nagyon idegesítő lesz a sokféle implementációval vacakolni.

Itt jön a képbe a Sys.Observer osztály, ami képes egy tetszőleges objektumot igazi publisherré tenni.

Vegyük a korábban már látott országokat egy tömbben:

  var countries = [
        { Name: 'Austria', Capital: 'Vienna' },
        { Name: 'Australia', Capital: 'Canberra' },
        // És így tovább...
    ];

És jelenítsük meg a már jól ismert DataView vezérlővel deklaratív módon:

  <ul 
    class="sys-template" 
    sys:attach="dv" 
    dv:data="{{ countries }}">
    <li>
        {{ Name }} ({{ Capital }}) 
    </li>
  </ul>

Készítsünk két linket, amivel a forrás adatokat manipuláló függvényeket fogjuk hívni:

  <a href="#" onclick="onAdd()">Hozzáad</a> 
  <a href="#" onclick="onDelete()">Töröl</a>

Ezek után, ha az onAdd és onDelete függvényekben megpróbáljuk közvetlenül módosítani a countries változót, akkor hiába várjuk, hogy a módosítások megjelenjenek a felületen. Nosza vessük be a Sys.Observer osztályt és tegyük a countries gyűjteményt observable-lé:

  Sys.Observer.makeObservable(countries);

Majd figyeljük meg például a Visual Studio Watch ablakában, hogy hogyan változott a saját kis primitív adatstruktúránk: Sys.Observer.makeObservable eredménybe

A gyűjteményünk kapott néhány metódust és ha ezeket használjuk az adatok módosítására, akkor arról minden subscriber értesülni fog. A metódusok nevei eléggé magukért beszélnek, egyedül annyit emelnék ki, hogy ha nem akarjuk az értesítést azonnal elküldeni – például mert több tulajdonságot módosítunk sorban – akkor a módosítások előtt hívjuk meg a beginUpdate, a végén pedig az endUpdate metódust. Az előbbi felfüggeszti az értesítés küldést, az utóbbi pedig elküldi őket. A makeObservable metódust egyszerűbb esetekben akár el is hagyhatjuk, ekkor azonban a Sys.Observer osztály “statikus” metódusainak első paraméteréül át kell adnunk, hogy melyik objektummal dolgozzanak.

Visszatérve a fenti példára, írjuk meg az onAdd és onDelete metódusokat az új metódusok segítségével:

  function onAdd()
  {
    countries.add({ Name: 'France', Capital: 'Paris' });
  }

  function onDelete()
  {
    countries.removeAt(9);
  }

Ezzel készen is vagyunk, ha kipróbáljuk látni fogjuk, hogy a gyűjtemény módosításai azonnal látszódnak a felhasználói felületen, nem kell külön frissítenünk. Már csak azzal vagyok adós, hogy miért? A megoldás pedig igen egyszerű: a DataView okos osztály, észreveszi, hogy observable adatforrással dolgozik és automatikusan feliratkozik a változásaira, nekünk ezzel nem kell foglalkoznunk.

Érdemes kipróbálni, hogy mi történik, ha nem a gyűjteményen, hanem a gyűjtemény egyik elemén módosítunk, például így:

  countries[7].Name = 'Magyarország';

Megmondom már most: semmi, a DataView rá se bagózik. De hogy mindennek mi köze a live bindinghoz, azt azonban csak legközelebb árulom el…

A cikkhez tartozó forráskód letölthető innen.

Advertisements

ASP.NET AJAX 4: Kliens oldali adatkötés – webszolgáltatáshoz

A cikksorozat előző részében bemutattam, hogyan használhatjuk az ASP.NET AJAX 4 kliens oldali DataView vezérlőjét helyi változókban tárolt adatok adatkötéssel történő megjelenítéséhez. Ebben a részben kiszakadunk a böngészőből és egy webszolgáltatástól kérjük le az adatokat.

A webszolgáltatás elkészítése

A feladat ugyanaz, mint az előző cikkben, országok listáját akarjuk megjeleníteni, ezért készítettem egy Country osztályt, amiben semmi logika nincs, csak összefogja a Name és Capital tulajdonságokat. Mivel ASMX webszolgáltatásból akarom használni, ezért fontos, hogy legyen neki paraméter nélküli konstruktora.

Ezek után készítettem egy CountryWebService.asmx nevű fájlt, benne a CountryService osztállyal. Fontos, hogy mivel AJAX-ból akarjuk meghívni ezt a webszolgáltatást, ezért nem csak a WebSerice, hanem a ScriptService attribútumot is rá kell ragasztanunk.

A webszolgáltatás osztályban készítettem egy privát List<Country> típusú mezőt, ami az adatbázisunkat reprezentálja, ezen fognak futni a kliens lekérdezések.

Eddig tehát semmi extra, itt tartunk:

  [WebService( Namespace = "http://balassy.spaces.live.com/Samples/" )]
  [WebServiceBinding( ConformsTo = WsiProfiles.BasicProfile1_1 )]
  [System.Web.Script.Services.ScriptService]
  public class CountryWebService : System.Web.Services.WebService
  {
    private List<Country> countries;

    public CountryWebService()
    {
        this.countries = new List<Country>()
        {
            new Country( "Austria", "Vienna" ),
            new Country( "Australia", "Canberra" ),
            // És még jó sok további Country...

        };
    }
  }

A WebMethodot úgy írtam meg, hogy paraméterként megkaphassa, hogy az országok neve vagy fővárosa szerint, illetve növekvő vagy csökkenő sorrendben kívánja a kliens megkapni az adatokat:

  [WebMethod]
  public Country[] GetCountries( string orderby, bool asc )
  {
    Func<Country, string> keySelector;        

    switch( orderby )
    {
        case "Name":
            keySelector = new Func<Country, string>( c => c.Name );
            break;
        case "Capital":
            keySelector = new Func<Country, string>( c => c.Capital );
            break;
        default:
            keySelector = new Func<Country, string>( c => c.Name );
            break;
    }

    IEnumerable<Country> result = asc ? 
this.countries.OrderBy( keySelector ) :
this.countries.OrderByDescending( keySelector ); return result.ToArray(); }

Ugyanez WCF-ben

Ha ugyanezt a webszolgáltatást nem ASMX-ben, hanem WCF-ben akarjuk elkészíteni, akkor kicsit többet kell dolgoznunk vele. Ugyanúgy szükségünk lesz egy CountryService osztályra és benne egy GetCountries metódusra, amiket szokás szerint meg kell jelölnünk a ServiceContract és OperationContract attribútumokkal. Továbbá mivel a szolgáltatás Country példányokat ad vissza, arra kellenek a DataContract és DataMember attribútumok.

Mivel a WCF szolgáltatásunkat AJAX-ból akarjuk meghívni, még nem vagyunk készen. Először is kell a szolgáltatás osztályunkra az AspNetCompatibilityRequirements attribútum:

  [AspNetCompatibilityRequirements( 
RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed )]

Továbbá – mint minden WCF szolgáltatáshoz, ehhez is – feltétlenül szükség van egy gigantikus méretű, alig átlátható konfigurációs XML-re a web.config fájlba:

  <system.serviceModel>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
    <behaviors>
        <endpointBehaviors>
            <behavior name="CountryServiceAspNetAjaxBehavior">
                <enableWebScript />
            </behavior>
        </endpointBehaviors>
        <serviceBehaviors>
            <behavior name="CountryServiceBehavior">
                <serviceMetadata httpGetEnabled="true" />
                <serviceDebug includeExceptionDetailInFaults="true" />
            </behavior>
        </serviceBehaviors>
    </behaviors>
    <services>
        <service behaviorConfiguration="CountryServiceBehavior" name="CountryService">
            <endpoint address="" binding="webHttpBinding" 
contract="CountryService"
behaviorConfiguration="CountryServiceAspNetAjaxBehavior" /> </service> </services> </system.serviceModel>

A sok beállítás közül a két legfontosabb a webHttpBinding és az enableWebScript.

Ha eddig készen vagyunk, akkor a kliensnek már mindegy, hogy milyen technológia dolgozik szerver oldalon.

Kliens oldali adatkötés deklaratívan

Az adatkötéshez kliens oldalon megint csak egy egyszerű HTML fájlt készítettem, benne script blokkokkal pedig betöltöttem az ASP.NET AJAX 4 JavaScript fájljait az ajax.microsoft.com CDN-ről. Az országok felsorolását most is ul-li listában végezzük el a kliens oldali DataView vezérlő segítségével, ami deklaratívan mindössze ennyi:

  <ul
      class="sys-template"
      sys:attach="dv"
      dv:autofetch="true"
      dv:dataprovider="Services/CountryWebService.asmx"
      dv:fetchoperation="GetCountries"
      dv:fetchparameters="{{ { orderby: 'Name', asc: true } }}" >
      <li>
          {{ Name }} ({{ Capital }}) 
      </li>
  </ul>

dv:dataprovider

A DataView dataProvider tulajdonságával adjuk meg, hogy honnan jönnek az adatok. Ez lehet egy sima JSONos webszolgáltatás URI, egy Sys.Net.WebServiceProxy példány vagy bármilyen osztály, ami implementálja a Sys.Data.IDataProvider interfészt, azaz van fetchData metódusa. Az utóbbira példa a Sys.Data.AdoNetServiceProxy osztály, amivel ADO.NET Data Services (Astoria) szolgáltatásokat hívhatunk meg, a legegyszerűbb esetben pedig közvetlenül rámutathatunk az .asmx vagy az .svc fájlra.

dv:fetchoperation

A DataView a fetchOperation tulajdonságban várja annak a metódusnak a nevét, amit a webszolgáltatáson meg akarunk hívni. Ide lehet query stringet is írni, ami ADO.NET Data Services (Astoria) esetén további lehetőségeket ad szűrésre, rendezésre, lapozásra.

dv:fetchparameters

A fetchParameters tulajdonságon keresztül paraméterezhetjük fel a meghívandó webmetódusunkat. Egyetlen JSON “dictionary”-t kell csak összeállítanunk.

dv:autofetch

Az autoFetch tulajdonság true-ra állításával adhatjuk meg, hogy a DataView az oldal betöltődésekor azonnal forduljon az adatforráshoz és töltse fel magát adatokkal. Ha ezt false-ra állítjuk, akkor nekünk kell meghívni a fetchData metódust, például így (feltételezve, hogy a fenti ul elemet elláttuk egy id=”list” attribútummal):

  $find('list').fetchData();

Kliens oldali adatkötés kódból

Természetesen itt is van lehetőség arra, hogy a HTML markupot és az adatkötést szétválasszuk, tehát a teljes adatkötést tisztán kódból végezzük el. Csupaszítsuk le a markupot (az előző cikkben bemutattam, hogyan lehet a {{}} elemektől megszabadulni, itt most csak az adatforrás kezelésére koncentrálok, ezért ezeket most meghagytam):

  <ul
    id="list"
    class="sys-template" >
    <li>
        {{ Name }} ({{ Capital }}) 
    </li>
  </ul>

Az adatkötést pedig az oldal inicializálásakor végezzük el, így:

  <script type="text/javascript">
    Sys.Application.add_init(appInit);
    function appInit()
    {
        $create(
            Sys.UI.DataView,
            {
              autoFetch: false,    // Hogy legyen lehetőségünk még inicializálni
              dataProvider: "Services/CountryService.svc",
              fetchOperation: "GetCountries",
              fetchParameters: { orderby: 'Name', asc: true }
            },
            null,
            null,
            $get("list")
        );
        // Ide jöhetne további inicializálás...
        $find('list').fetchData();
    }
  </script>

Ennyi, ki lehet próbálni, működni fog.

Rendezés

Miután rátettük a kezünket a fetchParameters tulajdonságra, semmi akadálya, hogy a felhasználó határozza meg, milyen sorrendben akarja látni az adatokat. Hogy egyszerűbb legyen az életünk, a rendezés adatait és a DataView vezérlőt kiemelhetjük globális fetchParams és dv változókba:

  var fetchParams = { orderby: 'Name', asc: true }
  var dv;
  Sys.Application.add_init(appInit);
  function appInit()
  {
      $create
( Sys.UI.DataView, { autoFetch: false,
dataProvider: "Services/CountryService.svc", fetchOperation: "GetCountries", fetchParameters: fetchParams }, null, null, $get("list") );
      dv = $find('list');
      dv.fetchData();
  }

Kellene még két metódus, amellyel gyorsan lehet rendezési szempontot és irányt váltani és amelyek azonnal frissítik a DataView vezérlőt:

  function sortBy(col)
  {
    fetchParams.orderby = col;
    dv.fetchData();
  }

  function sortAsc(dir)
  {
    fetchParams.asc = dir;
    dv.fetchData();
  }

Végül már csak UI elemekre van szükség ezeknek a függvényeknek a meghívásához:

  <a href="#" onclick="sortBy('Name')">Név szerint</a> 
  <a href="#" onclick="sortBy('Capital')">Főváros szerint</a> 
  | 
  <a href="#" onclick="sortAsc(true)">A-Z</a> 
  <a href="#" onclick="sortAsc(false)">Z-A</a>

Rakjuk össze mindezt egy oldalra és a felhasználó boldogan kattintgathat a linkekre, a háttérben a megfelelő sorrendben fognak letöltődni az adatok és azonnal meg is jelennek a böngészőben – természetesen AJAXosan frissítve az oldalnak csak az érintett részét.

A szép az egészben, hogy a tudomány nem áll meg itt: a rendszer képes arra is, hogy észrevegye, ha egy változó értéke megváltozik és automatikusan frissítse az értéket a felhasználói felületen. Folyt. köv.

A cikkhez tartozó forráskód letölthető innen.