sobota, 15 maja 2010

Wzorzec Post/Redirect/Get w ASP.NET MVC

Każdy(a) z Was pewnie spotkał się komunikatem "Aby wyświetlić tę stronę, Firefox musi ponownie przesłać dane etc..." O co chodzi i co zrobić, aby w naszych systemach tak nie było?

Podobny komunikat pokazuje, że odwiedzany przez nas serwis Web ma poważny problem, jakim jest ponowne wysyłanie POST. Schemat jest taki:

1. Przeglądarka wysyła polecenie GET i otrzymuje stronę z formularzem.
2. Użytkownik go wypełnia i klika "Wyślij".
3. Przeglądarka wysyła z użyciem POST dane do aplikacji.
4. Aplikacja odsyła stronę, że operacja się udała.
5. Użytkownik chce przeładować stronę z komunikatem.
6. Przeglądarka wysyła ostatnie zapytanie. Jakie? To z POST, z punktu 3.
7. Aplikacja drugi raz wykonuje akcję związaną z formularzem.

Czyli my chcemy zrobić coś normalnego (przeładować stronę, kliknąć "Wstecz"), a aplikacja zrobi coś więcej niż od niej oczekujemy. Hmmmm. A ciekawie robi się dopiero, kiedy ktoś używa przeglądarki, która nie ostrzega przed ponownym wysłaniem POST, takiej jak IE6.

Jak sobie z tym poradzić?

Z pomocą przychodzi wzorzec Post/Redirect/Get :) Zasada jest prosta - wynikiem POST nie może być strona, a przekierowanie do strony z GET:

1. Przeglądarka wysyła polecenie GET i otrzymuje stronę z formularzem.
2. Użytkownik go wypełnia i klika "Wyślij".
3. Przeglądarka wysyła z użyciem POST dane do aplikacji.
4. Aplikacja odsyła przekierowanie na stronę z wynikiem.
5. Przeglądarka pobiera ją używając GET.
6. Aplikacja odsyła stronę, że operacja się udała.
7. Użytkownik chce przeładować stronę z komunikatem.
8. Przeglądarka wysyła ostatnie zapytanie. Jakie? To z GET, z punktu 5.
9. Aplikacja ponownie wysyła stronę z wynikiem, a nie wykonuje ponownie akcji.


Dobra, tyle teoria. A jak wzorzec PRG zastosować w przykładowym frameworku opartym o wzorzec Model-Widok-Kontroler, takim jak ASP.NET MVC 1.0?

Jeżeli mamy akcję kontrolera reagującą na POST, to nie może ona zwracać widoku z wynikiem - musi przekierowywać do akcji GET, która to dopiero zwróci widok z wynikiem.

Mówiąc uniwersalnym językiem kodu, jeżeli mamy taki kod kontrolera (pomijam całą logikę, sprawdzanie poprawności itp.):

public ActionResult WypiszKsiazki()
{
  return View(Repository.ListAllBooks());
}

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult DodajKsiazke(string title)
{
  Repository.AddBook(title);
  Repository.Submit();
  ViewData["Message"] = "Dodanie zakończone sukcesem";
  return View("WypiszKsiazki", Repository.ListAllBooks());
}


To właśnie tak nie powinno być :) OK, to robimy Redirect...

public ActionResult WypiszKsiazki()
{
  return View(Repository.ListAllBooks());
}

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult DodajKsiazke(string title)
{
  Repository.AddBook(title);
  Repository.Submit();
  ViewData["Message"] = "Dodanie zakończone sukcesem";
  return RedirectToAction("WypiszKsiazki");
}


To nadal będzie źle - zaginie nam komunikat o sukcesie. Tutaj warto wykorzystać zamiast ViewData strukturę TempData: :)

public ActionResult WypiszKsiazki()
{
  ViewData["Message"]= TempData["Message"];
  return View(Repository.ListAllBooks());
}

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult DodajKsiazke(string title)
{
  Repository.AddBook(title);
  Repository.Submit();
  TempData["Message"] = "Dodanie zakończone sukcesem";
  return RedirectToAction("WypiszKsiazki");
}


Voila, gotowe! :) Tym niemniej ze względu na to, że trzeba dane "przepychać" przez TempData, to o Post/Redirect/Get najlepiej pomyśleć na samym początku :) A nie tak jak ja :P

Więcej o wzorcu PRG: http://en.wikipedia.org/wiki/Post/Redirect/Get

4 komentarze:

  1. Fajne! Tylko masz małego buga na ostatniej pozycji drugiej listy. ;)

    OdpowiedzUsuń
  2. Poprawiony :) W sumie miałem jeszcze 6 minut luzu :D

    OdpowiedzUsuń
  3. Paweł z Głuchołaz pisze:

    Dlaczego nikt nie zauważył że zrobiłem w Site.Master ...ViewData["Message"] ?? TempData["Message"]...?

    W ten sposób nie trzeba robić tego okropnego przepisywania z TempData do ViewData a widok załatwia sprawę sam :)

    OdpowiedzUsuń
  4. Ha, jak sprytnie :) Np. ja nie zauważyłem, bo site.master nie czytuję na bieżąco :)

    OdpowiedzUsuń