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.














