سؤال

This is probably going to be a whopper...

Ok so I'm building a MVC4 website that has a moderately-frequent theme of being able to edit a parent record as well as add/delete/edit child records on the same page. Using the magic of MVC I am able to define a partial view for the child records like so:

@model NFBC.Models.SubMap

<tr id="@String.Concat("SubMap", ViewBag.Index)">
    <td class="mapname">
        <input type="hidden" name="submaps[@ViewBag.Index].Id" value="@Model.Id" />
        <input type="text" name="submaps[@ViewBag.Index].MapName" value="@Model.MapName" />
    </td>
    <td class="miles"><input type="text" name="submaps[@ViewBag.Index].Miles" value="@Model.Miles" /></td>
    <td class="difficulty">@Html.DropDownList("submaps[" + (string)ViewBag.Index.ToString() + "].DifficultyId", (SelectList)ViewBag.Difficulty(Model.DifficultyId))</td>
    <td class="elevation"><input type="text" name="submaps[@ViewBag.Index].Elevation" value="@Model.Elevation" /></td>
    <td class="mapfile"><input type="text" name="submaps[@ViewBag.Index].MapFile" value="@Model.MapFile" /></td>
    <td class="delete"><img src="~/Images/Error_red_16x16.png" /></td>
</tr>

Then in the parent view I simply call the partial view to render all of its children:

<table id="ChoicesTable">
    <thead>
        <tr>
            <th>Map Name</th>
            <th>Miles</th>
            <th>Difficulty</th>
            <th>Elevation</th>
            <th>Map File</th>
            <th></th>
        </tr>
    </thead>
    for (int i = 0; i < Model.SubMaps.Count; i++)
    {
        var map = Model.SubMaps.ElementAt(i);
        ViewBag.Index = i;
        Html.RenderPartial("_MapChoiceEditRow", map);
    }
</table>

I am not able to find any documentation on the "sub entity" name syntax (ie: name="subMaps[@ViewBag.Index].Id"), but it works when binding to a model; all of the children are filled in so long as the indexe values start at 0 and aren't missing any values (ie: 0, 1, 2, 4, 5 will result in binding just 0, 1, and 2). Using the magic of jQuery's Ajax call I am able to dynamically insert and delete rows on the client side.

The problem is that I simply cannot figure out a way to reliably use @Html.EditorFor() with the child entity controls. This would be really nice functionality to have since EditorFor injects all of the unobtrusive jquery validation attributes into the html. Right now I'm basically forced to emulate this behavior by adding my own "data-val='true'" tags (not shown in example, I haven't done it yet) all over the place, which to me seems extremely messy.

So I had the brilliant idea of taking the built-in templates and creating my own templates to inject this stuff (as well as some other things of my own, such as Bootstraps "placeholder" attribute for "helper" text, and maybe tooltips, etc). I downloaded the MVC source and opened up the default editor templates, but instead of seeing markup that renders the unobtrusive values, instead I just get a whole bunch of helper functions that at some point "magically" render the unobtrusive attributes. I cannot figure out how it's done, since the validation stuff is all packed into internal classes that aren't accessible to me.

Am I missing something here or is this just a weakness of MVC that I'm going to have to work around. I'd really love to not need to emulate the unobtrusive validation attribute generation code on my own, but if it's the only solution I suppose I could do it...

Thanks!

هل كانت مفيدة؟

المحلول

I spent the afternoon playing around with the source code some more, and discovered several key helper methods that allowed me to do what I needed to do.

It's not the prettiest thing in the world, but it goes a long way towards automating what I was doing before. My solution was to create a new TextBoxFor helper method called TextBoxForChild:

    public static MvcHtmlString TextBoxForChild<TModel, TProperty>(
            this HtmlHelper<TModel> htmlHelper,
            Expression<Func<TModel, TProperty>> expression,
            string parentName,
            int index,
            IDictionary<string, object> htmlAttributes)
    {
        var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
        var rules = ModelValidatorProviders.Providers.GetValidators(metadata, htmlHelper.ViewContext).SelectMany(v => v.GetClientValidationRules());

        if (htmlAttributes == null)
            htmlAttributes = new Dictionary<String, object>();
        if (!String.IsNullOrWhiteSpace(metadata.Watermark))
            htmlAttributes["placeholder"] = metadata.Watermark;
        UnobtrusiveValidationAttributesGenerator.GetValidationAttributes(rules, htmlAttributes);

        return htmlHelper.TextBox(String.Format("{0}[{1}].{2}", parentName, index, metadata.PropertyName), metadata.Model, htmlAttributes);
    }

I get the "ModelMetadata" object, then I generate the "model validator rules" for the model. Then I fill in my custom "placeholder" html attribute with the watermark metadata value, and finally call "UnobtrusiveValidationAttributesGenerator.GetValidationAttributes", which fills my html attributes dictionary with all of the validation attributes.

I then do some custom string formatting to make sure the input name follows the MVC child entity format, and voila, it works!

FYI in case anyone was wondering where that "Watermark" value comes from, it's a value from "DisplayAttribute", called "Prompt":

public class FamilyMember
{
    public int ClubNumber { get; set; }

    [Display(Name="Name", Prompt="Name")]
    [Required]
    public string Name { get; set; }
    public string Cell { get; set; }
    public string Email1 { get; set; }
    public string Email2 { get; set; }
    public DateTime? BirthDate { get; set; }
}

Lovely!

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top