Validatie zoals UX het wil, met ASP.NET MVC 2
This post was originally published on Coding Glamour.
De standaard validatie in MVC 2 gaat er vanuit dat je werkt met 'Validation
messages' achter je tekstvelden, en een 'Validation summary'
bovenaan je scherm. Dit werkt snel en out-of-the box, inclusief javascript
validatie via jQuery Validate, en ook server side wanneer er geen javascript
beschikbaar is (pic via).
Maar... wat als je UX-afdeling dit spuuglelijk vindt? Vandaar dat we bovenstaande
gaan transformeren naar:
Zo veel mogelijk standaard
Om ervoor te zorgen dat we zo min mogelijk werk doen, conformeren we aan
het model dat MVC afdwingt met annotations op je velden:
[Required(ErrorMessage = "\"Naam\" is verplicht maar niet ingevoerd")]
[DisplayName("Naam")]
public string Naam { get; set; }
Wanneer we nu in de view hier een textbox voor tekenen krijgen we er extra
gratis validatie bij:
<%= Html.TextBoxFor(m => m.Naam) %>
Punt is alleen dat onze classes voor niet geldige velden niet op
de textbox zitten maar op een veld eromheen:
<!-- valide veld -->
<span class="input-wrap">
<input type="text" />
</span>
<!-- niet valide veld -->
<span class="input-wrap input-error">
<input type="text" />
</span>
HTML Helper voor de wrapper
De wrapper om het veld heen kunnen we uiteraard ook genereren, en daarbij
meteen de metadata over de correctheid van het veld meenemen. Dat kan door
middel van iets als:
<%= Html.WrapperFor (m => m.Naam) %>
<%= Html.TextBoxFor (m => m.Naam %>
<%= Html.EndWrapper %>
Maar dat is vrij foutgevoelig! Tijd voor een trucje met 'IDisposable':
<% using (Html.BeginInputWrapperFor (m => m.Naam)) { %>
<%= Html.TextBoxFor (m => m.Naam %>
<% }
We kunnen nu in de constructor van het wrapper element de begin tag schrijven,
en in de 'Dispose' de eindtag.
public static class HtmlHelperExtender
{
public static MvcInputWrapper BeginInputWrapperFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
{
// al deze metadata helpers zitten standaard in MVC!
return new MvcInputWrapper(htmlHelper, ModelMetadata.FromLambdaExpression<TModel, TProperty>(expression, htmlHelper.ViewData), ExpressionHelper.GetExpressionText(expression));
}
}
// class zelf
public class MvcInputWrapper : IDisposable
{
// cache tagbuilder en textwriter
private readonly TagBuilder _builder;
private readonly TextWriter _writer;
// ctor
public MvcInputWrapper(HtmlHelper htmlHelper, ModelMetadata modelMetadata, string expression)
{
// controleer of het veld valide is
ModelState state;
bool isValid = true;
var fullHtmlFieldName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(expression);
if (htmlHelper.ViewData.ModelState.TryGetValue(fullHtmlFieldName, out state) && (state.Errors.Count > 0))
{
isValid = false;
}
// cache de writer
_writer = htmlHelper.ViewContext.Writer;
// maak een tagbuilder aan
_builder = new TagBuilder("span");
_builder.AddCssClass("input-wrap");
// als niet valid? voeg de error class toe
if(!isValid) _builder.AddCssClass("input-error");
// schrijf begin tag
_writer.Write(_builder.ToString(TagRenderMode.StartTag));
}
public void Dispose()
{
// schrijf eind tag
_writer.Write(_builder.ToString(TagRenderMode.EndTag));
}
}
Validation summary
Bovenaan moet ook nog een validation summary komen te staan. Ook hier
kunnen we weer handig gebruik maken van de ModelState functies die beschikbaar
zijn in de HtmlHelpers:
// in je view
// <%= Html.ValidationSummaryFunda %>
public static string ValidationSummaryFunda<TModel>(this HtmlHelper<TModel> htmlHelper)
{
// zet client validation aan, zodat we dat niet handmatig hoeven te doen
htmlHelper.EnableClientValidation();
var modelState = htmlHelper.ViewData.ModelState;
// als het model valid is, dan sowieso return ""
if (modelState.IsValid) return "";
// pak alle error messages
var errorMessages = modelState.Values.Where(v => v.Errors.Any()).SelectMany(v => v.Errors).Select(v=>v.ErrorMessage);
// en bepaal aan de hand van het aantal messages de uiteindelijke foutmelding
MvcHtmlString errorString;
if(errorMessages.Count() == 1)
{
errorString = MvcHtmlString.Create(string.Format("<strong>{0}.</strong> Vul het rode veld aan.", errorMessages.First()));
}
else
{
errorString = MvcHtmlString.Create("<strong>Meerdere velden zijn niet correct ingevoerd.</strong> Vul de rode velden aan.");
}
// bouw de tag
var builder = new TagBuilder("p");
builder.AddCssClass("notify-error");
builder.InnerHtml = errorString.ToString();
// en render
return builder.ToString(TagRenderMode.Normal);
}
Validatie in de controller
Om te zorgen dat de error-messages ook daadwerkelijk te zien worden wanneer
je submit zal je nog een check moeten toevoegen in je action:
[HttpGet]
public ViewResult Contact(SoortAanbod aanbod, ObjectdetailPagina tab, long id)
{
// dit is de GET logica om het formulier te tonen
var model = new ObjectsClient().GetContact(aanbod, tab, id);
return View(Enum.GetName(tab.GetType(), tab), model);
}
[HttpPost]
public ActionResult Contact(SoortAanbod aanbod, ObjectdetailPagina tab, long id, ObjectContactModel model)
{
// na POST
// als modelstate niet valid
if (!ModelState.IsValid)
{
// doe de GET actie. ModelState blijft hier geldig, dus je raakt je validatie-info niet kwijt
return Contact(aanbod, tab, id);
}
// handel normaal af
var url = new ObjectsClient().SaveContact(id, model, tab);
return Redirect(url);
}
Client side validatie
Dit werkt prima, maar alleen server-side. Voor client-side validatie laat
ik de volgende keer zien hoe je eenvoudig ASP.NET MVC 2 kan koppelen aan
een (eigen) validatie-framework in javascript.
There are 10 comments on this article, read them on Coding Glamour.