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?