poniedziałek, 11 czerwca 2012

Boje z ASP.NET MVC AJAX i pragma no-cache

Dziś napotkaliśmy w pracy dość ciekawy problem który nie dał się rozwiązać szybko na pierwszy rzut oka.

Pisząc portal w ASP.NET MVC, używając widoków Razor, postanowiłem zdynamizować nieco stronę główną, i zastąpić linki generowane przez helper @Url.Action, linkami pochodzącymi z helpera @Ajax.ActionLink.

Linki te to taka bezpieczna ajaxowa nakładka na nawigację - jest to pełnoprawny, normalny link, który jest indeksowalny dla wyszukiwarek, jednak jeżeli użytkownik ma włączoną obsługę javaScriptu, wtedy kliknięcie w link spowoduje wejście na url linka za pomocą Ajax, i wyświetlenie zawartości w obiekcie o zadanym ID.

Używam tych linków w kluczowych miejscach portalu którym aktualnie się zajmuję - w części w której większość użytkowników spędzi dużo czasu, więc warto zainwestować w ergonomię i pozwolić użytkownikom np. na przeglądanie newsów bez nieco deprymującego odświeżenia strony w przeglądarce.

Ponieważ Ajax.ActionLink prowadzi pod ten sam adres URL, co jego nieajaxowa wersja, ASP.NET posiada metodę obiektu Request dostępnego w każdym kontrolerze i widoku: Request.IsAjaxRequest(). Jeśli ta metoda zwróci true, to oznacza to że widok został użyty w akcji kontrolera która nie została załadowana zwykłym GET-em, tylko właśnie za pomocą ajax.

Dzięki temu, możemy zadecydować, czy widok ma zwrócić całą stronę, wraz z jej parent layoutem, czy tylko samą treść, ponieważ zapytanie jest ładowane przez ajax, i parent layout już istnieje.

przykładowy widok, który może być odczytywany w dwójnasób: zwykłym linkiem GET, oraz ajaxem.


@{
ViewBag.DontDrawMainPagePiece = true;
ViewBag.Title = _LayoutResources.Actuals;
if (Request.IsAjaxRequest() == false)
{
Layout = "~/Views/Shared/Layouts/AjaxLayout.cshtml";
}
else { Layout = null; }
}
@Html.Action( "NewsListPartial", new { id = ViewBag.PageId } )





natomiast AjaxLayout.cshtml zawiera coś takiego


[......]

<div class="content_container" id="ajax_container">
@RenderBody()
</div>
[......]


przykładowy ajax link który działa jako get na stronie bez JS i jako ajax w pełni kompatybilnej przeglądarce, współpracujący z tym wygląda tak:

@{var updateTargetId="ajax_container";}

@Ajax.ActionLink( "link_text", Model.ActionName, Model.ControllerName,
Model.RouteParameters, new AjaxOptions
{
LoadingElementId = "loader_image",
UpdateTargetId = updateTargetId,
OnBegin = String.Format("fadeOut('{0}')",updateTargetId),
OnComplete = String.Format("fadeIn('{0}')", updateTargetId),
HttpMethod = "Post"
} )

taki piękny link, wygeneruje nam coś co odwoła się do akcji z modelu, natomiast wszystko co ten ajax request zwróci, zostanie załadowane do obiektu DOM o id #ajax_container. Dlatego ajax_container jest na zewnatrz, w pliku AjaxLayout.cshtml - jezeli strona jest odczytywana po get - musi zostac zrenderowana z calosci razem z otaczającym treść htmlem. treść znajduje sie w divie o id #ajax_container.

Jesli klikniemy w ajaxlink, to to co zostanie zwrocone zostanie załadowane do diva ajax_container.
Poniewaz jest to request ajaxowy, a w widoku zawarliśmy stwierdzenie że, jeżeli request jest ajaxowy, to layout= null, zostanie zwrócona sama treść, bez kolejnego ajax_container, dzięki czemu przegladarka nie zglupieje. Wszystko sie zgadza, layout pozostal bez zmian, zmieniła się sama treść.


No i tak zrobiliśmy. Po publishu sie okazało że strona rozkraczyła się pod Internet Explorerem 9. Każda przeglądarka interpretowala link poprawnie, i wyświetlała treść poprawnie, natomiast internet Explorer umieszczał w ajax_container, zrenderowaną w całości strone, wraz z otaczającym ją layoutem, strona w stronie, menu 2 razy, efekt opłakany. Pod każą inną przeglądarką wszystko działało poprawnie. So confusing... błąd jest ewidentnie po stronie serwera więc jak może go powodować inna przeglądarka?

Sprawdziliśmy snifferem Fiddler co się dzieje na łączach. Okazało się że z jakiegoś powodu prawie wszystkie akcje w responsach zwracają pragma: no-cache, jednak strona główna z jakiegoś powodu nie. I ta drobna różnica stała się przyczyną błędu i zamieszania.

Uruchomiłem debugger, rozstawiłem breakpointy. Internet explorer wykonywał request na Home/Index, ten go przekierowywał do właściwej strony, jednak, on miał już u siebie w cache skeszowaną wersję tej strony, pobraną przy pierwszym wejściu na stronę główną (za pomocą GET), wraz z całym layoutem (choć tym razem request był ajaxowy, i pomimo że ASP.NET, już się wziął za ponowne zrenderowanie tego widoku (tym razem bez tej całej otoczki zawartej w ajaxLayout.cshtml), Internet explorer zwrócił stronę z cache, wraz z menu, stopką, nagłówkiem...).

Natomiast np google chrome czy mozilla, ponieważ w piewszym response nie było wyraznego polecenia cache-owania danego URL-a, nie robiły tego, i strona ładowała się jak należy.

Problem wynika z tego że IE oraz reszta różnie interpretują parametr w nagłówku HTTP o nazwie pragma.
Internet explorer, gdy go nie znajdzie, to zakłada że strone trzeba scache-ować.
Natomiast reszta przegladarek zakłada że gdyby developer chcial by strona była cache-owana, to by umiescił tam pragma-cache i jakąś wartość.

powyższy kawałek kodu w global.asax, w event handlerze PreRequestExecuteHandler załatwia sprawę:

this.Response.AddHeader( "Cache-Control", "no-cache, no-store, max-age=0, must-revalidate" );

od tej pory wszystkie strony funkcjonują jednakowo we wszystkich przeglądarkach pod względem cache.

Brak komentarzy:

Prześlij komentarz