2011. szeptember havi bejegyzések

Visual Studio 11 – feature No1

Biztos sokan már elkezdték nézegetni a múlt heti konferencián kiadott Visual Studio 11 Developer Preview-t. Nem tudom, ki hogy van vele, számomra az egyik legfontosabb újdonság, hogy végre nem konvertálódnak automatikusan a projekt fájlok, amikor a Visual Studio új verziójával megnyitjuk a kódot, így korábbi VS verziókkal továbbra is dolgozhatunk rajta. Mindezt kombinálva azzal, hogy egy gépre a Studio több verziója is telepíthető, óriási előrelépés, nem véletlenül tették ezt a Product Highlights lista elejére:

Project and solution backward compatibility. Open and edit projects and solutions created by using Visual Studio 2010 in Visual Studio 11 Developer Preview without converting the Visual Studio 2010 project first. The project and solution can then be opened again in Visual Studio 2010, as long as you have not implemented any code that has a dependency on any Visual Studio 11 Developer Preview-only features. For more information, see Installing Visual Studio Versions Side-by-Side.

A fiúk tehát Redmondban figyelnek a visszajelzésekre.

 

Technorati-címkék: ,

Unable to find assembly Microsoft.IdentityModel

Dávid Zoli barátommal épp egy HTML 5-ös, MVC-s, Azure-os, Facebook és Live loginos projekten dolgozunk, ahol a bejelentkezést a Windows Azure AppFabric Access Control Service (a marketingesek úgy látszik karakterszám alapján kapják a fizetést) segítségével oldottuk meg. Mivel ez lényegében Windows Identity Foundation az alkalmazás szempontjából, ezért a WIF telepítését megúsztuk annyival, hogy a Microsoft.IdentityModel.dll-t szépen bemásoltuk a website bin mappájába. Éljen a bin deployment!

Ez szépen is működött, ám amint elkezdtük használni a blog storage-ot, az alábbi hibaüzenet fogadott:

Unable to find assembly ‘Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35’.

Hasonló néha előfordul, de ez a hibaüzenet két ok miatt furcsa:

  1. Semmi nem változott, továbbra is ott csücsül pont ez a DLL a bin-ben.
  2. A hibaüzenet nem egy authentikációval vagy authorizációval kapcsolatos kódrészletnél jött, hanem egy Azure blobos RoleEnvironment.IsAvailable hívásnál.

A Windows Azure hibaelhárítási tippek között szerepel egy idevonatkozó is, miszerint telepítsem újra a WIF-et. Csakhogy én soha nem telepítettem és nem is szeretném!

Végül a megoldás az lett, hogy gacutil /i-vel bedobtuk a DLL-t a GAC-ba, ami a fejlesztői gépen megoldotta a hibát, de az éles környezetbe történő telepítésnél sajnos le kell mondanunk bin deploymentről Szomorú arc

 

Számított mező típusa

Vajon milyen típusú lesz az IsComplete számított mező?

CREATE TABLE [dbo].[Member]
(
    DisplayName nvarchar(100) NULL,
    Email nvarchar(100) NULL,
    IsComplete AS ( CASE WHEN DisplayName IS NOT NULL AND 
                              Email IS NOT NULL THEN 1 
                         ELSE 0 
                    END ) PERSISTED
)

Elárulom, int lesz a nyomorult, amit nem is vethetünk a szemére, hiszen sehol nem mondtuk neki, hogy bit legyen.

Így viszont tutira bit lesz:

  IsComplete AS CAST( CASE WHEN DisplayName IS NOT NULL AND 
                                Email IS NOT NULL THEN 1 
                           ELSE 0 
                      END AS bit ) PERSISTED

Technorati-címkék:

Így bootol a Windows 8 mindössze 2 másodperc alatt

Érdemes megnézni az alábbi hivatalos Microsoft videót arról, hogy mennyivel kellemesebb lesz a Windows 8 boot ideje a korábbi verziókhoz képest. Ne pislogj közben, mert lemaradsz:

Hát igen, tényleg sokkal gyorsabban fog elindulni a következő Windows verzió, tisztelet és gratuláció a fejlesztőknek!

De amikor arról álmodozunk, hogy az otthoni gépünket majd ilyen villámgyorsan fogjuk elindítani, gondoljunk az alábbiakra:

  • A videón egy olyan szűz gép szerepel, amin még nincs telepítve kismillió extra szoftver és hardver, ami lassítja az életünket. És a te gépeden?
  • A videón lévő gépben nem BIOS van, hanem UEFI. És a te gépedben?
  • A videón lévő gépben nem forgó HDD van, hanem SSD. És a te gépedben?
  • A csajszi kivette az aksit a gépből, ami valljuk be, nagyon meggyőzőnek tűnik, de jelen esetben inkább nagyon David Copperfieldes. Valójában ugyanis nem a klasszikus értelemben vett hidegindítás történik, hanem a hagyományos hidegindítás és a hibernálás keveréke, amin a kivett aksi nem változtat.

Nagyon profi, amit a Windows 8 csapat véghezvitt, ugyanis jelentősen megváltoztatták a boot folyamatot annak érdekében, hogy nekünk, felhasználóknak jobb legyen. Az ilyen architekturális módosítások természetesen a mostani, BIOS-os, hagyományos HDD-s gépeket is segíteni fogják, de amikor a két másodpercről álmodunk, akkor ne felejtsük el, hogy almát hasonlítunk össze körtével. Idővel persze, ahogy cserélődnek a gépek, egyre közelebb kerülünk majd az áhított pillantásnyi időhöz.

Akit érdekel, hogy ez hogyan lehetséges, annak feltétlenül ajánlom a Building Windows 8 blog idevágó bejegyzését.

 

Technorati-címkék:

Excel linkek sütivel

Meglepő dolgok történnek, amikor felhasználóként egy Excel munkalapon lévő linkre kattintunk. Valószínűleg fel sem tűnik, hogy mi történik a háttérben, amíg nem próbálunk egy olyan oldal megnyitni, ami csak bejelentkezés után érhető el. Újabb mizéria a sütikkel.

Hozzá vagyunk szokva, hogy ha egy Excel dokumentumban rákattintunk egy linkre, akkor a kedvenc böngészőnk megnyitja az oldalt. A látvány alapján nyugodtan gondolhatjuk, hogy ez a folyamat játszódik le:

Excel-cookie-1

 

  1. A linkre kattintás után az Excel átadja a shellnek az URL-t, ami elindítja az alapértelmezett böngészőt.
  2. A böngésző megnyitja a kért URL-t.
  3. A webszerver visszaküldi a HTML választ.

Logikus nem? Az, csakhogy nem ez történik a valóságban!

Az elmélet és a gyakorlat közötti különbség akkor tűnik fel, amikor a kért oldalt authentikáció védi. Azt gondolnánk, hogy ha korábban be voltunk jelentkezve a böngészőnkben és megvan az authentikációs süti, akkor a második lépésben a böngésző azt szemrebbenés nélkül hozzácsapja a kéréshez, így az Excelből indított kéréseink is a bejelentkezett felhasználó nevében fogják elérni a szervert. Fájdalom, de nem.

A valóság – mint mindig – egy kicsit bonyolultabb, az Excel ugyanis megpróbál a böngésző helyett dolgozni:

Excel-cookie-2

  1. A linkre kattintás után az Excel küld egy HTTP kérést a kívánt URL-re a webszervernek.
  2. A webszerver visszaküldi a kért dokumentumot az Excelnek.
  3. Az Excel átadja a shellnek az URL-t, ami elindítja az alapértelmezett böngészőt.
  4. A böngésző megnyitja a kért URL-t.
  5. A webszerver visszaküldi a kért dokumentumot a böngészőnek.

Először tehát az Excel próbálja meg letölteni a kért erőforrást a webszerverről, és ha sikerült (HTTP 200 OK a válasz), akkor nyitja csak meg a böngészőt, hogy a felhasználó is lássa az eredményt. Szomorú, de a jelek szerint valójában kétszer jön át a kért fájl a hálózaton. Hogy ennek mi a pontos oka, azt nem tudom, de tény, hogy ha bármilyen hiba van – hálózat, HTTP 40x-50x – akkor nem nyílik meg a böngésző, hanem a hibaüzenet közvetlenül az Excelben jelenik meg. Lehet, hogy ez így felhasználóbarátabb.

Ezzel még így együtt is tudnánk élni, de kellemetlen meglepetések érhetnek, ha a hivatkozott erőforrás csak bejelentkezés után érhető el. Ekkor a következő történik:

Excel-cookie-3

  1. A linkre kattintás után az Excel küld egy HTTP kérést a kívánt URL-re a webszervernek.
  2. Mivel az erőforrás védett, a webszerver egy HTTP 302 Redirect válaszkóddal átirányít a Login.aspx oldalra, query stringben átadva az eredeti URL-t.
  3. Az Excel lekéri a Login.aspx oldalt, query stringben átadva az eredeti URL-t.
  4. A webszerver visszaküldi a Login.aspx HTML kódját HTTP 200 OK válasszal.
  5. Az Excel átadja a shellnek a redirect választban hivatkozott URL-t, azaz a Login oldal URL-jét a query stringgel.
  6. A böngésző megnyitja a Login.aspx oldalt, query stringben átadva az eredeti URL-t.
  7. A webszerver visszaküldi a Login oldal HTML kódját.

Az Excel megint a böngésző helyett dolgozik: először lekéri a kért erőforrást, de ha az máshol van, akkor már csak az átirányított URL-t adja át a böngészőnek. Ez sok esetben a bejelentkezést megvalósító Login oldal. Azonban amikor a 6. lépésben a böngésző megnyitja ezt az URL-t, akkor már lelkesen mellé csapja a login cookie-t, így a webszerver azt fogja látni, hogy a felhasználó már be van jelentkezve. De akkor mit keres a login oldalon?

Mit tehetünk ilyenkor a login oldalon:

  • Ha a felhasználó már be van jelentkezve és van returnUrl paraméter a query stringben, szó nélkül átirányítjuk oda. Újabb redirect, villanás és töltés, de végül meg fogja kapni a felhasználó a kívánt oldalt.
  • Hibaüzenet, hogy a login oldal nem bejelentkezett felhasználóknak való. Talán ez a legbarátságtalanabb megoldás.
  • Megjelenítjük a login oldalt, hátha más felhasználó nevében akar belépni, de akkor vigyázzunk, hogy a fejlécben, vagy máshol ne szerepeljen az aktuálisan belépett felhasználó neve.

Ez így látszólag működik is, de csak akkor, ha a login oldal nem azzal kezdi a pályafutását, hogy az előzőleg bejelentkezett felhasználó nyomait teljesen kiírtja (sütik törlése, session lezárása stb.).

Egy kicsit kellemetlenebb a helyzet, ha a kért oldalt ugyan bejelentkezés nélkül is el lehet kérni, de csak bizonyos feltételek esetén, például ha az oldalhoz tartozó entitás állapota ezt engedélyezi (például csak akkor, ha “jóváhagyott” státuszban van) vagy ha a felhasználó be van jelentkezve (például admin). Ekkor ugyanis tipikusan nem az ASP.NET gondoskodik az erőforrás védelméről, hanem az oldal forráskódja ellenőrzi, hogy a kérés és az üzleti objektum összhangban van-e egymással. És ha nem, akkor mit lehet tenni, át lehet irányítani egy “Nincs jogod az oldal megtekintéséhez” hibaoldalra.

Helyettesítsük be ezt a fenti hét lépcsős folyamatba:

  1. A linkre kattintás után az Excel küld egy HTTP kérést a kívánt URL-re a webszervernek.
  2. Mivel az erőforrást nem szabad megjeleníteni, a weboldal egy HTTP 302 Redirect válaszkóddal átirányít a PermissionDenied.aspx egyedi hibaoldalra.
  3. Az Excel lekéri a PermissionDenied.aspx oldalt.
  4. A webszerver visszaküldi a hibaoldal tartalmát HTTP 200 OK válasszal.
  5. Az Excel látja, hogy ez a jó cím, ezért átadja a shellnek a PermissionDenied.aspx oldal URL-jét.
  6. A böngésző megnyitja a PermissionDenied.aspx oldalt, de a kéréshez mellékeli az authentikációs sütit is.
  7. A webszerver visszaküldi a hibaoldal kódját.

Valójában tehát a kliens meg sem próbálja úgy letölteni a fájlt a szerverről, hogy a felhasználó bejelentkezését igazoló sütit odatenné a kérés mellé! Ha az oldal fejlécében ráadásul az is szerepel, hogy ki az aktuális felhasználó, akkor azt fogja látni a felhasználó, hogy be van jelentkezve, de nincs joga megtekinteni az oldalt. Képzeljük el, mit szól ehhez egy admin!

Ha nem akarjuk, vagy nem tudjuk átírni az ellenőrző logikánkat, akkor a megoldást egy kliens oldali átirányítás jelentheti:

Excel-cookie-4

Nem írom le az összes lépést, a lényeg, hogy az Excelből egy olyan URL-re hivatkozunk (1. és 4. lépés), amire a szerver válaszul egy olyan META fejléces HTML markupot küld vissza (2. és 5. lépés), aminek hatására aztán végül a böngésző irányít át a valódi URL-re (6. lépés). Ezzel kijátszottuk az Excel okoskodását és biztosak lehetünk abban, hogy a böngésző biztosan oda fogja tenni a kérés mellé az authentikációs sütit.

Ez a cikk nem jött volna létre, ha nincs a Fiddler.

 

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: ,,,,