Sunday, April 29, 2012

ASP.NET MVC Editor Templates

EditorTemplates reminds me of college days using assembly language. Copying bytes using MOV and LOOP instruction could get the job done, but not knowing the simpler way(REP MOVSB) to do this solved problem makes your code not as readable or maintainable as it could possibly be. Seeing many loops in code and deciphering their intent is counter-productive.


As much as we want to believe in this axiom "If At First You Don't Succeed, Remove All Evidence You Ever Tried", there's something to be said for knowing how a bad code looks like. With this in mind, this is not what to do in an ASP.NET MVC view:


~/Views/Home/Index.cshtml
@model SoQna.ViewModels.QnaViewModel

@using (Html.BeginForm("SubmitAnswers", "Home"))
{
    int i = 0;
    foreach (SoQna.ViewModels.AnswerToQuestion answer in Model.Answers)
    {
        @: Question #@(answer.ToQuestionId) <br />

        @Html.Hidden("Answers[" + i + "].ToQuestionId", answer.ToQuestionId)
        @Html.DisplayFor("Answers[" + i + "].QuestionText", answer.QuestionText)

        <p />

        @Html.TextArea("Answers[" + i + "].AnswerText", answer.AnswerText)

        <hr />
        
        ++i;
    }

    <input type="submit" value="Done" />
}

Sure that code is very model-centric and capable of being directly usable by our controller code...


// POST /Home/SubmitAnswers

[HttpPost]
public ViewResult SubmitAnswers(SoQna.ViewModels.QnaViewModel a)
{
    foreach (SoQna.ViewModels.AnswerToQuestion answer in a.Answers)
    {
        answer.QuestionText = _qnaRepo.Single(x => x.QuestionId == answer.ToQuestionId).QuestionText;
    }
    return View(a);
}


...but the problem with the ~/Views/Home/Index.cshtml view above is we cannot use strongly-typed model on html helpers. As much as possible, with strongly-typed framework such as ASP.NET MVC, we should not use magic strings in our code. We should let strong typing take over the reins in our ASP.NET MVC app. With this in mind, we shall do this instead on ~/Views/Home/Index.cshtml:

@model SoQna.ViewModels.QnaViewModel

@using (Html.BeginForm("SubmitAnswers", "Home" ))
{    
    @Html.EditorFor(x => x.Answers) 
    <input type="submit" value="Done" />
}

Now you might ask, where's the loop? How does it know how to display the corresponding HTML for our object's properties?

On first question, Html.EditorFor does the loop for us if the property is an IEnumerable one. On second question, that's where we will use the EditorTemplates. When you want to use a pre-defined view(editor template) for a given model or view-model, you place that view in this folder ~/Views/Shared/EditorTemplates, but if you intend your pre-defined view for a given model/view-model be a controller-specific one, placed them in their specific controller folder, e.g. ~/Views/XXX/EditorTemplates where XXX is your controller name. ASP.NET MVC will first look for editor templates specific to your controller; if it cannot find one, it will look in ~/Views/Shared/EditorTemplates folder


To make our first editor template, please create an EditorTemplates folder on ~/Views/Home, given Home is your controller name. Then add an MVC 3 Partial Page (Razor) item to your ~/Views/Home/EditorTemplates folder, you do this by doing a right-click on EditorTemplates and selecting Add > New Item

Name your editor template Razor page after your model type or view-model type.

This is how our ~/Views/Home/EditorTemplates/AnswerToQuestion.cshtml shall look like:

@model SoQna.ViewModels.AnswerToQuestion

Question #@(Model.ToQuestionId) <br />
@Html.HiddenFor(x => x.ToQuestionId)
@Html.DisplayFor(x => x.QuestionText)

<p />

@Html.TextAreaFor(x => x.AnswerText)

<hr />

Here's a sample output:



Download: http://code.google.com/p/aspnet-mvc-demo-editor-templates/downloads/list

SVN: http://code.google.com/p/aspnet-mvc-demo-editor-templates/source/checkout

No comments:

Post a Comment