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:

without

Jak widać tracimy nasz dodany element, a tego nie chcemy. Wystarczy zamienić EditorFor na RenderPartial i to rozwiążę naszą sprawę:

with

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:

client

Jeżeli wypełnimy pole danymi i dodamy kolejnego autora, powinniśmy zobaczyć następujący screen:

server

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:

brak

Ż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:

afterSave

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.