W poprzedniej części opisałem w jaki sposób możemy generować listy elementów do edycji/tworzenia tak by nie przejmować się numerycznym indeksem. To co nam jednak pozostało to sprawa walidacji danych.
Najlepszym przykładem będzie zmodyfikowanie kodu, który już posiadamy (najnowsza wersja na github) – dodajmy atrybut Required dla własności Name w CreateAuthorViewModel. Następnie odpalmy projekt, wejdźmy na zakładkę Books i potem kliknijmy Create New.
Jeżeli teraz klikniemy na Add book author i nie wprowadzimy nic klikając na Create nastąpi uderzenie do serwera, w którym kontroler sprawdzi czy model jest poprawny (walidacja udana) i jeżeli nie jest to zostanie nam zwrócone po raz kolejny to samo view.
Teraz jeżeli wykorzystaliśmy EditorFor zamiast RenderPartial w widoku dla generowania elementów dynamicznie, to dostaniemy widok następujący:
Jak widać tracimy nasz dodany element, a tego nie chcemy. Wystarczy zamienić EditorFor na RenderPartial i to rozwiążę naszą sprawę:
Jednak pozostaje wciąż jeden problem.
Wykonajmy tą samą zmianę dla EditAuthorViewModel – dodajmy atrybut Required, upewnijmy się, że wykorzystujemy RenderPartial zamiast EditorFor, otwórzmy stronę, wejdźmy w edycję Sandkings, wyczyśćmy pole autora książki i kliknijmy Save.
Od razu pojawi się nam problem z walidacją pola Name, oraz zauważmy, że dodatkowa informacja o tym, że należy naprawić wszystkie błędy przed kliknięciem kolejnym na Save nie jest wyświetlona:
Jeżeli wypełnimy pole danymi i dodamy kolejnego autora, powinniśmy zobaczyć następujący screen:
Oznacza to, że walidacja po stronie klienta nie została wykonana, widać wyraźnie, że pojawił się nam opis błędu z serwera. Jeżeli przyjrzymy się temu z poziomu FireBug to zobaczymy:
Że brakuje nam atrybut walidacyjny niezbędnych do tego by walidacja po stronie klienta działała. Co ciekawsze po tym jak to pójdzie do serwera to atrybuty dla walidacji zostaną wygenerowane:
Rozwiązaniem tego problemu jest to co już swojego czasu Procent opublikował u siebie na blogu. Podmiana FormContext na new załatwia problem i nasze atrybuty będą generowane. Jednakże ja osobiście lubię kiedy moje View nie zawierają takich elementów – jest to tak jakby logika, muszę o tym pamiętać i przy czyszczeniu kodu nie kasować. Osobiście takie podejście mi nie odpowiada – jest szybkie i zgrabne, ale wymaga ode mnie myślenia i pamiętania. Lepszym rozwiązaniem jest możliwość zwrócenia już gotowego View z poziomu kontrolera.
W projekcie jak zauważyliście istnieje BaseController, teraz z niego trochę skorzystamy. Zanim jednak do tego dojdzie musimy utworzyć własny ActionResult. Celem naszego ActionResult będzie zrobienie tego samego co Procent z poziomu View. Ja swój ActionResult nazwałem DynamicViewResult a jego kod wygląda tak:
using System.Globalization; using System.Text; namespace System.Web.Mvc { public class DynamicViewResult : ViewResultBase { protected override ViewEngineResult FindView(ControllerContext context) { ViewEngineResult result = ViewEngineCollection.FindPartialView(context, ViewName); ViewContext viewContext = new ViewContext(context, result.View, ViewData, TempData, context.HttpContext.Response.Output); using(new UnobtrusiveFormContext(viewContext, FormId)) { if(result.View != null) { return result; } StringBuilder builder = new StringBuilder(); foreach(string str in result.SearchedLocations) { builder.AppendLine(); builder.Append(str); } throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "The [partial] view '{0}' was not found or no view engine supports the searched locations. The following locations were searched:{1}", ViewName, builder)); } } public string FormId { get; set; } } }
Wykorzystuje w nim klasę UnobtrusiveFormContext:
namespace System.Web.Mvc { public class UnobtrusiveFormContext : IDisposable { private readonly ViewContext _context; public UnobtrusiveFormContext(ViewContext context, string formId) { _context = context; _context.ClientValidationEnabled = true; _context.UnobtrusiveJavaScriptEnabled = true; _context.FormContext = new FormContext { FormId = formId }; } public void Dispose() { _context.OutputClientValidation(); } } }
Teraz do BaseController dodałem następujące dwie metody:
protected virtual DynamicViewResult DynamicView(string viewName, object model) { return DynamicView(null, viewName, model); } protected virtual DynamicViewResult DynamicView(string formId, string viewName, object model) { if(model != null) { ViewData.Model = model; } DynamicViewResult result = new DynamicViewResult(); result.FormId = formId; result.ViewName = viewName; result.ViewData = ViewData; result.TempData = TempData; return result; }
Zaś akcję zwracającą mi moje View zamieniłem na:
public virtual ActionResult EditRowForBookAuthor(string prefix) { ViewData[NestedPrefix] = prefix; return DynamicView(Views._EditBookAuthorRow, new EditAuthorViewModel()); }
Dzięki temu przy dodawaniu nowego elementu dynamicznie, nasz input będzie zawierał atrybuty walidacyjne dla walidacji po stronie klienta.
Jednakże wciąż pozostaje jeden problem – mimo posiadania tych atrybutów, walidacja po stronie klienta nie działa do póki nie nastąpi uderzenie do serwera. Jednak tym, zajmiemy się następnym razem.
Zmodyfikowana wersja kodu znajduje się na github.