Ajax címkéhez tartozó bejegyzések

Átirányítás a bejelentkező oldalra Ajaxnál

A hitelesítés korrekt megvalósításának egyik legproblémásabb része annak megoldása, hogy mi történjen akkor, ha az alkalmazáshoz a szükséges engedélyek nélküli kérések érkeznek. Ez így nagyon bénán hangzik, úgyhogy íme egy példa: a /admin URL-re bejelentkezés nélkül nem érkezhetnek kérések, sőt még azokat a kéréseket is el kell utasítani, amik bejelentkezett felhasználóhoz tartoznak, de a felhasználó nincs az Admins csoportban.

ASP.NET esetén szerencsére a FormsAuthenticationModule modul gondoskodik a probléma megoldásáról. Az OnLeave eseménykezelővel feliratkozik az EndRequest eseményre, és ha a HTTP státusz kód 401, akkor átirányítja a felhasználót a bejelentkező oldalra. Ez egy kiváló szolgáltatás hagyományos kérések esetén, de kifejezetten kellemetlen Ajaxnál.

Az átirányítás hatására ugyanis a kliens nem kapja meg a 401 Unauthorized hibát, hanem egy 302 Redirect fejléc fog visszajönni a szervertől, amit az XMLHttpRequest kliens oldalon az előírásnak megfelelően transzparens módon követ, azaz beküld egy kérést a Location fejlécben megadott útvonalra. Ez az útvonal tipikusan a Login.aspx oldalra mutat, aminek a szerver visszaküldi a HTML kódját, és ezt a HTML kódot fogja megkapni az XHR az Ajax hívás eredményeként 200 OK státusz kóddal. Ember legyen a talpán, aki ezt a helyzetet kliens oldalon értelmesen kezelni tudja.

Sajnos .NET 4.0-ig nem volt más megoldás a probléma kezelésére, mint egy saját HTTP modul beillesztése az ASP.NET csővezetékbe. Az ASP.NET 4.5 azonban bevezetett egy új lehetőséget a HttpResponse.SuppressFormsAuthenticationRedirect tulajdonság formájában, amit true értékre állítva elkerülhető az átirányítás, és helyette az eredeti 401-es hibakódot fogja megkapni a kliens. Mivel ez a tulajdonság a Response része, ezért nem tudjuk globálisan beállítani, hanem minden olyan kérésnél be kell billenteni, ahol el akarjuk kerülni az átirányítást. Ha esetleg minden kérésnél így akarunk eljárni, akkor tehetjük ezt a tulajdonság állítást a global.asax fájlban lévő Application_EndRequest eseménykezelőbe.

Miután a kliens megkapja az egyértelmű hibakódot, JavaScriptből már úgy járhatunk el, ahogy az igényeink megkívánják, például feldobhatunk egy hibaüzenetet vagy egy bejelentkező ablakot. De ez a kód már eddig is megvolt, nem igaz?

 

Technorati-címkék: ,,

JSON sorosítás

Már régóta nyilvánvaló, hogy az AJAX a múlté, hiszen AJAJ van helyette. Kezdetben azért volt ez így, mert kín és szenvedés volt az XMLHttpRequest objektumot alacsonyszinten matatni, de aztán jött a jQuery és azóta minden rózsaszín. Az XML-t azonban teljesen jogosan leváltotta a JSON és azóta az Ajax elnevezés már nem is egészen helytálló.

Persze mióta mindenki JSON-t használ a kliens-szerver közötti kommunikációra, egyre gyakrabban merül fel a kérdés, hogy hogyan állítsunk elő szerver oldalon JSON-t és hogyan dolgozzuk fel a JSON-ban bejövő adatokat? .NET-ben szerencsére (?) van erre több lehetőség is:

Már az is elég szomorú, hogy egy frameworkön belül két osztály is szolgál ugyanarra, a nagyobb gond viszont az, hogy egyik sem tökéletes. Egyrészt sajnos mindkettőben vannak hibák, amik valahogy nem akarnak kijavulni több framework verzió óta, másrészt egyszerűen nem elég rugalmasak. Szóval rájuk férne egy kis pofozgatás, webfejlesztői szemmel mindenképp. Tudja ezt az ASP.NET MVC csapat is, csakhogy egyik névtér sem hozzájuk tartozik, így jár, aki out-of-band.

De kár szomorkodni, hiszen vannak kiváló megoldások a piacon, sőt a szabadpiacon! Ott van például a Json.NET osztálykönyvtár, ami már sok helyen bizonyított, ahol a beépített osztályok kevésnek bizonyultak. Ingyenes, rugalmas és még gyors is:

json-406-json-performance

Le a kalappal James Newton-King előtt, hogy egy ilyen hobbiból megírt és még szabadon elérhetővé is tette. Köszönjük.

Akkor most egy pillanatra képzeljük magunkat az MVC csapat helyébe: van egy funkció, ami nagyon kell, ám a beépített verzió nem ideális és nincs lehetőségük megjavítani, ámde van egy külső komponens, ami jónak tűnik. A külső komponensek amúgy is beváltak már ennél a csapatnál, elég csak a jQuery-re, jQuery Validationre és Modernizr-re gondolni, amik megjelennek a projekt sablonokban.

Hogy mi lesz a preferált megoldás az MVC4-ben, arról kár most találgatni (és amúgy se mondhatom meg az NDA miatt). Én mindenesetre azt mondom, aki JSONozik, annak ideje megismerni a Json.NET-et…

 

Technorati-címkék: ,,,

Ajax hívásnál 401 Unauthorized

Érdekes hibával találkoztam a héten. Adott egy jQuery-vel meghívott WebMethod, ahonnan a HTTP kérések kiválóan kimennek, de a válasz mindig ez:

{ "Message":"Authentication failed.",
  "StackTrace":null,
  "ExceptionType":"System.InvalidOperationException" }

Különlegességként a válaszban még érkezik egy jsonerror: true fejléc sor is.

Amit ilyenkor célszerű ellenőrizni:

  • Jogosultságok a web.configban ne tiltsák a hozzáférést.
  • Ha esetleg kell Session is, akkor a szerver oldalon a WebMethod attribútumnál legyen beállítva az EnableSession=true tulajdonság.
  • Ha a szerver oldali metódus ASPX-ben van, akkor legyen static.
  • És a ráadás: az ASMX fájlban lévő @WebService direktíva Class tulajdonsága az .ASMX.CS fájlban lévő névtérre és osztályra mutasson. Ez utóbbi azért huncut, mert a Solution Explorerben az .asmx fájlra kattintva az .asmx.cs nyílik meg, nincs fordítási hibaüzenet és a futás idejű hibából sem derül ki, hogy ez a gond.

 

Technorati-címkék: ,,,,

Ajaxos táblázatfrissítés ASP.NET MVC-ben JavaScript nélkül

A HTML5 Játéktér fejlesztésénél jött elő a feladat, hogy egy Ajax hívás után frissíteni kell egy táblázatot, pontosabban törölni kell egy sort belőle. Ha az adott sorban a felhasználó rákattint a Törlés gombra, akkor törölni kell a megfelelő rekordot a szerveren, majd a kliens oldalon a táblázatból is, természetesen teljes page postback nélkül. A jó hír az, hogy ezt ASP.NET MVC-ben egyetlen sor JavaScript írása nélkül meg lehet tenni.

Íme a view egy részlete, ami a táblázat egy sorát rendereli:

<tr id="game-@item.Slug">
  <td>
    @Ajax.ActionLink( "Törlés", "Delete", "Game",
      new { slug = item.Slug },
      new AjaxOptions
      {
        Confirm = "Biztos?", 
        InsertionMode = InsertionMode.Replace, 
        UpdateTargetId = "game-" + item.Slug
      },
      new { @class = "button delete" } )
  </td>
</tr>    

A lényeges részeket aláhúztam:

  • Minden sornak generálunk egy egyedi id attribútumot.
  • Az Ajax hívásnál megadjuk, hogy a szervertől érkező válasszal cserélje ki (InsertionMode) az egész sort (UpdateTargetId).

Ha a feladat az aktuális sor eltávolítása, akkor csak arról kell gondoskodnunk, hogy a szerver oldali controller action üres válasszal térjen vissza:

public ActionResult Delete( string slug )
{
  // Ide jön a törlés...

  return new EmptyResult();
}

 

Technorati-címkék: ,,

ASP.NET MVC remote validation

A model validation az egyik kedvencem az ASP.NET MVC-ben. Egy helyre kerül minden, ami a validáláshoz kell, ráadásul a jQuery validationnek köszönhetően a beépített “validátorok” nagyon jól működnek kliens oldalon is.

Legyen mondjuk egy klasszikus Person modell osztályunk, amire könnyen felaggathatom a validálási szabályokat leíró attribútumokat:

public class Person
{
    [DisplayName( "E-mail cím:" )]
    [Required( ErrorMessage = "Az e-mail cím megadása kötelező! ")]
    public string Email { get; set; }

    [DisplayName( "Felhasználónév:" )]
    [Required( ErrorMessage = "A felhasználónév megadása kötelező! " )]
    public string UserName { get; set; }

    [DisplayName( "Jelszó:" )]
    [Required( ErrorMessage = "A jelszó megadása kötelező! " )]
    public string Password { get; set; }
}

A view-ba pedig csak @Html.ValidationMessageFor hívások kellenek a hibaüzenetek megjelenítéséhez (amiket egyébként a VS le is generál magától):

@using( Html.BeginForm() )
{
    @Html.ValidationSummary( true )
    <fieldset>
        <legend>Regisztráció</legend>
        <div class="editor-label">
            @Html.LabelFor( model => model.Email )
        </div>
        <div class="editor-field">
            @Html.EditorFor( model => model.Email )
            @Html.ValidationMessageFor( model => model.Email )
        </div>
        <div class="editor-label">
            @Html.LabelFor( model => model.UserName )
        </div>
        <div class="editor-field">
            @Html.EditorFor( model => model.UserName )
            @Html.ValidationMessageFor( model => model.UserName )
        </div>
        <div class="editor-label">
            @Html.LabelFor( model => model.Password )
        </div>
        <div class="editor-field">
            @Html.EditorFor( model => model.Password )
            @Html.ValidationMessageFor( model => model.Password )
        </div>
        <p>
            <input type="submit" value="Mehet" />
        </p>
    </fieldset>
}

A helyzet akkor kezd bonyolódni, amikor kliens oldalon szeretnénk validálni, de az input ellenőrzési logika vagy a hozzá szükséges egyéb adatok szerver oldalon vannak, például egy adatbázisban. Még kellemetlenebb a helyzet, ha ráadásul nem is egyetlen mező értékét kell ellenőriznünk, hanem több adat együttállását kell vizsgálnunk. Ezt megoldhatnánk egy postback után a controllerben, de sokkal felhasználóbarátabb, ha Ajaxosan oldjuk meg, hiszen így már az űrlap kitöltése közben tudjuk figyelmeztetni a felhasználót, hogy javítsa ki a beírt értékeket. Szerencsére erre a problémára van közvetlen megoldás az ASP.NET MVC-ben, úgy hívják, hogy remote validation. Így kell beizzítani:

A modellben adjunk egy Remote attribútumot a validálandó mezőhöz:

[Remote( "ValidateUserNameAndEmail", "Home", AdditionalFields = "Email" )]
public string UserName { get; set; }

Itt meg kell adnunk, hogy melyik controller action lesz az Ajax hívás szerver oldali végpontja. Ha több mező értékére van szükségünk szerver oldalon, akkor használhatjuk az AdditionalFields paramétert. A fenti példában a validálás akkor fog megtörténni, amikor a UserName mező értéke megváltozik és a szerverhez el fog jutni az Email mező értéke is, az esetleges hibaüzenet pedig a UserName mező mellett fog megjelenni. Szerencsére semmi akadálya annak, hogy ugyanezt az attribútumot ráakasszuk az Email mezőre is, AdditionParameterkent a UserName mezőt adva meg.

Már csak a hivatkozott controller actiont kell megírnunk, például így:

[OutputCache( NoStore = true, Location = OutputCacheLocation.None )]
public JsonResult ValidateUserNameAndEmail( string userName, string email )
{
    return this.IsValid( userName, email ) ? 
        this.Json( true, JsonRequestBehavior.AllowGet ) : 
        this.Json( "Ezzel a névvel és e-mail címmel már létezik regisztráció!", 
            JsonRequestBehavior.AllowGet );
}

A valódi ellenőrző logikát áttettem egy IsValid függvénybe, hogy itt ne zavarjon. A lényeg:

  • Klasszikus controller action azokkal a paraméterekkel, amiket ellenőrizni szeretnénk. Ha van a modellben AdditionalFields, akkor mindet.
  • Válaszként egy JsonResult típussal kell visszatérnünk.
  • Ha az adat helyes, akkor a válasz true, ha hibás, akkor a hibaüzenet.
  • Meg kell adni a JsonRequestBehavior.AllowGet paramétert, anélkül nem fog menni.
  • Célszerű az OutputCache-t kikapcsolni, hogy a böngésző ne gyorsítótárazza a választ.

A hibaüzenet a Html.ValidationMessageFor helyén fog megjelenni, tehát a view-n nem kell változtatnunk. Ha nem a Studio generálta a markupot, hanem mi írtuk, akkor arra érdemes figyelni, hogy legyen benne Html.BeginForm, mezei <form> elemre tapasztalataim szerint nem működik.

Mindössze ennyi, az Ajaxos bűvészkedést az ASP.NET megcsinálja helyettünk. Íme az eredmény:

aspnet-mvc-remote-validation

Akit szeretné lerántani a leplet a varázslatról, az nézze meg a generált markupban a data-val-remote- attribútumokat és figyelje meg a HTTP forgalmat például Fiddlerrel. Érdemes!

 

Technorati-címkék: ,,,,,

Golyóálló Ajax hívások

Korábban írtam már arról, hogy nagyon egyszerűen tudunk ASP.NET-es page method-okat hívni jQuery segítségével. No, de mi van akkor, ha valamilyen nem várt hiba történik közben?

Nézzük lépésről lépésre, kezdjük először azzal, hogy hogyan is megy ez. Először is legyen egy link, ami elindítja az Ajax hívást, és egy span, ami majd megjeleníti az eredményt:

  <a href="#" id="simpleLink">Egyszerű Ajax hívás</a>
  <span id="result"></span>

Legyen egy WebMethod az oldalon belül, amit ajaxosan meg akarunk hívni a linkre kattintáskor:

  [WebMethod]
  public static string SayHello( string name )
  {
    return "Hello " + name;
  }

Az Ajax híváshoz persze jQuery-t használunk, szerencsére így nagyon rövid a JavaScript kód:

  $("#simpleLink").click(function (e) {
    e.preventDefault();

    $.ajax({
      type: "POST",
      url: "Default.aspx/SayHello",
      data: "{ 'name':'World' }",
      dataType: "json",
      contentType: "application/json; charset=utf-8",
      success: function (data) {
        $("#result").html(data.d);
      }
    });
  });

Hol lehet ezzel probléma? Néhány tipikus eset:

  • Bármiféle alacsonyszintű (hálózati) hiba keletkezik.
  • A szerver oldali kódból kivétel röppen a kliensre.
  • Lejár a felhasználó login sessionje, ezért az Ajax hívás authentikációs hibával száll el, mert nem jut el a szerver oldali végpontig.
  • Lejár a felhasználó munkamenete, ezért a szerver oldali kód érvénytelen Session értékekkel dolgozik.

Ezek között van könnyebb és nehezebb eset. Az alacsonyszintű hibákat lehet kezelni egy error callbackben kliens oldalon, szerencsére a jQuery támogatja.

A szerver oldalon keletkező kivétel már rosszabb, mert ennyire barátságos válaszból kell dolgoznia a kliensnek, ami egy HTTP 500 Internal Server Error társaságában jön vissza:

  {
    "Message":"Baj van!",
    "StackTrace":"   at _Default.SayHello(String name) 
       in w:\\System\\Desktop\\AjaxWrapperSample\\Default.aspx.cs:line 14",
    "ExceptionType":"System.InvalidOperationException"
  }

A példában itt egy “nem várt” eset szerepel (hiszen az InvalidOperationException általában erre utal), de mihez kezdünk akkor, ha az üzleti logikánk is kivétellel jelzi, ha valamit nem sikerült végrehajtani? Például mert a felhasználónak nincs hozzá jogosultsága, vagy megváltozott az üzleti objektum állapota, karbantartás alatt van az alkalmazásszerver stb.

A lejárt login session kifejezetten kellemetlen, mert klasszikus HTTP 200 OK tud visszajönni, csak éppen a tartalomban nem a kért adatokat küldi vissza a szerver, hanem a bejelentkező oldalunk HTML markupját. Talán erre számít legkevésbé az ember.

Ezeket természetesen mind lehet kezelni szerver és kliens oldalon is, a nehézséget az jelenti, hogy ezt minden egyes Ajax hívásnál meg kell tennünk kliens és szerver oldalon is. Célszerű lenne olyan megoldást találnunk, ami nem igényli az összes WebMethodunk és az összes kliens oldali hívásunk szétbarmolását, hanem központilag tudjuk valahogy kezelni a hibákat. És itt jön be a képbe az a szemérmetlenül kellemetlen tény, hogy Ajax hívások esetén az ASP.NET klasszikus központi eseménykezelői (pl. Application_Error) nem hívódnak meg…

Az alábbi megoldást az egyik projektünkben használjuk és eddig bevált. A módszer lényege, hogy az eredeti kódot becsomagoljuk és a csomagolás feladata a részletek elrejtése mind kliens, mind szerver oldalon.

Első lépésként definiáltuk az alábbi osztályt:

  public class AjaxResult<TResult>
  {
    public bool Success { get; set; }
    public TResult Value { get; set; }
    public string Error { get; set; }
  }

Ennek az osztálynak a feladata, hogy burkolja az eredeti választ a Value paraméterben és további információkat csapjon hozzá, amit a kliens felhasználhat az egyedi hibakezeléshez. Itt a példában csak két paramétert vettem fel, a Success jelzi, ha a hívás szerver oldalon nem okozott kivételt, az Error pedig az esetleges hibaüzenetet (ezeket persze lehetne egyben is, de a bool és a string külön nekem szimpatikusabb).

A következő feladat a szerver oldali csomagolás elkészítése olyan módon, hogy az eredeti kódra a legkisebb hatással legyen. Erre az alábbi megoldás született:

  public static class AjaxWrapper
  {
    public static AjaxResult<TResult> Execute<TResult>( Func<TResult> body )
    {
      try
      {
        // Inicializálás, szerver konfigurálás itt...

        return new AjaxResult<TResult>
        {
          Success = true, 
          Value = body()
        };
      }
      catch( Exception ex )
      {
        return new AjaxResult<TResult>
        {
          Success = false,
          Error = ex.GetType().Name
        };
      }
    }
  } 

Oké, a Func miatt elsőre talán nem egyértelmű, hogy mi történik, ezért gyorsan megmutatom, hogyan kell használni és máris világos lesz a jelentése. A fenti SayHello metódusból ez lett:

  [WebMethod]
  public static AjaxResult<string> SayHelloSafe( string name )
  {
    return AjaxWrapper.Execute( () =>
    {
      return "Hello " + name;
    });
  }

A kód lényegi része tehát megmaradt, mindössze egy AjaxWrapper.Execute hívást kell köré csapni. Siker esetén a lényegi rész eredménye bekerül a Value tulajdonságba, hiba esetén pedig a kivétel típusa kerül az Error tulajdonságba. Ezzel elértük azt, hogy a kódunkban keletkező összes szerver oldali hibát elkapjuk és barátságos formában juttatjuk a kliensre.

Már csak ki kell csomagolnunk a választ kliens oldalon, amivel egyúttal a klasszikus d tulajdonságtól is megszabadulhatunk. Ehhez jó lesz az alábbi JavaScript függvény:

  function myAjax( url, params, successCallback, errorCallback ) {
    $.ajax({
      type: "POST",
      url: url,
      data: params,
      dataType: "json",
      contentType: "application/json; charset=utf-8",
      success: function (response, status, xhr) {
        if (response.d.Success) {
          successCallback(response.d.Value, status, xhr);
        }
        else {
          errorCallback(response.d.Error, status, xhr);
        }
      },
      error: function (xhr, status, error) {
        if (xhr.status === 401) {
          alert("Lejárt a munkameneted, lépj be újra!");
          window.location.href = "Login.aspx";
          return;
        }

        errorCallback(xhr, status, error);
      }
    });            
  }

Itt egyúttal megoldhatjuk a lejárt login session esetét is. Ha szerver oldalon az AjaxWrapper.Execute metódusban kitalálunk egy módszert a lejárt Session kezelésére, akkor ezt az esetet is kezelhetjük itt központilag.

Végezetül csak arra kell figyelnünk, hogy mostantól a $ajax helyett mindenhol ezt a myAjax függvényt használjuk:

  myAjax(
    "Default.aspx/SayHelloSafe",
    "{ 'name':'World' }",
    function (response) {
        $("#result").html(response);
    },
    function (response) {
        $("#result").html('Gáz van: ' + response);
    }
  );

Kicsit egyszerűsítettem a kódon, élesben egy kicsit bonyolultabb, de ez a lényeg. A teljes forráskód letölthető az MSDN Kompetencia Központ oldaláról.

Ti hogyan kezelitek ezeket a kivételesen kellemetlen eseteket?

 

Technorati-címkék: ,,,,

WCF és REST – ahol mindenre van URI

Az alkalmazásintegráció területén a SOAP mellett egyre nagyobb szerepet kapnak a REST alapú megoldások. Mert kicsi, egyszerű, gyors. Egyszóval fapados. Nem csak az elkésztése, hanem a meghívása is, ami különösen akkor érdekes, ha a klienseink butábbak az átlagnál, például Ajaxos weboldalak vagy épp telefonok.

Ezt a területet a Microsoft már évekkel ezelőtt célba vette, és kiadta hozzá a WCF REST Starter Kitet. Még sosem hallottál róla? Hát valóban nem ez lett a világ leghíresebb terméke, de legalább elindította a céget egy olyan irányba, amelynek komoly mérföldköve lett a .NET4 és a héten a MIX’11 konferencián bejelentett WCF Web API.

A WCF Web API a WCF Futures csomagban érhető el, amely a következő területekre fókuszál:

  • REST – ez a WCF Web API
  • Kliensek – Ajax, jQuery, Silverlight, telefon
  • Felhő
  • Egyszerűsítés

Ezek közül a MIX-en két területtel kapcsolatban jelent meg újdonság.

1. A REST területen elérhetővé vált a WCF Web API Preview 4 verziója. Ennek az a célja, hogy az alkalmazásaink funkcióit egyszerűen nyithassuk meg HTTP felett a REST alapelveit követve. Itt egy olyan architektúrát kell elképzelni, ahol minden a HTTP-re van kihegyezve:

WCF-Web-API

2. A kliensek területén megjelent a RIA/JS, ami gyakorlatilag közvetlen WCF támogatást jelent a jQuery-hez:

WCF-jQuery

Ez még elég gyerekcipőben jár, de a BigShelf példa alkalmazáson már érezhető, hogy a $.dataSource és a DataLink jQuery pluginek mennyire egyszerűsítik a megírandó kliens oldali kódot.

Aki web API fejlesztéssel foglalkozik, annak mindenképp érdemes megnéznie az újdonságokat, mert a jelek szerint a REST a jövő.

 

Technorati-címkék: ,,