Search code examples
asp.net-mvchtml-helpermvc-editor-templates

Making my own HtmlHelper extension for input that works with model binding


I am unhappy with the current DropDownList implementation, because I can't really do much with the option tags (only selected, text and value is supported). I want to make my own where I can set disabled and other stuff on individual options.

Currently I'm altering the options by javascript, but I think it's a bit of a hacky way to do it, and I'd prefer to just render the correct html to begin with.

I know I can just make a template that uses select and option tags and make the options as I want them - but the normal DropDownList extension adds stuff val stuff and a specific name and ID which I guess is for proper databinding when submitting the form:

<select data-val="true" data-val-number="The field SelectedValue must be a number." id="ParentDropDown_SelectedValue" name="ParentDropDown.SelectedValue">

How do I go about adding these attributes to my own templates?


Solution

  • You are right, those attributes (and specially the name attribute) are critical for the model binding.

    Say you want to create a custom helper like

    public static MvcHtmlString CustomHelperFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
    

    First you can use var fieldName = ExpressionHelper.GetExpressionText(expression); to get the field name.

    Then use var fullBindingName = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(fieldName); in order to get the full name, taking care of nested views.

    Finally you can transform this into an id attribute using var fieldId = TagBuilder.CreateSanitizedId(fullBindingName);.

    So a simple custom helper that creates a textbox could be written as:

    public static MvcHtmlString CustomHelperFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
    {            
        var fieldName = ExpressionHelper.GetExpressionText(expression);
        var fullBindingName = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(fieldName);
        var fieldId = TagBuilder.CreateSanitizedId(fullBindingName);
    
        var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
        var value = metadata.Model;
    
        TagBuilder tag = new TagBuilder("input");
        tag.Attributes.Add("name", fullBindingName);
        tag.Attributes.Add("id", fieldId);
        tag.Attributes.Add("type", "text");
        tag.Attributes.Add("value", value == null ? "" : value.ToString());
    
        var validationAttributes = html.GetUnobtrusiveValidationAttributes(fullBindingName, metadata);
        foreach (var key in validationAttributes.Keys)
        {
            tag.Attributes.Add(key, validationAttributes[key].ToString());
        }
    
        return new MvcHtmlString(tag.ToString(TagRenderMode.SelfClosing));
    }
    

    You can use it in a view like:

    @Html.CustomHelperFor(model => model.ParentDropDown.SelectedValue)
    

    And it will produce the following html:

    <input id="ParentDropDown_SelectedValue" name="ParentDropDown.SelectedValue" type="text" value="4">
    

    Hope it helps!