11
\$\begingroup\$

My goal was to be able to render my view models using a standard Object.cshtml editor and display template. In order to do so I needed to be able to always call Html.Editor(propertyName) or Html.Display(propertyName) in order to render the HTML element.

There were plenty of resources for creating custom attributes and custom editor templates. However I ran into a wall when creating the DropDownListAttribute and template since I needed to obtain the values for the select list as well as set the selected item based on the current instance of the view model.

Here is the reference I used to create the custom ModelMetadataProvider and custom attributes.

Here is the problem I ran into with getting the current instance of the view model.

It turns out using reflection here to access the view model is not safe. Depending on how you iterate over the property metadata the view model can sometimes be null.

Here is my implementation using the resource referenced above. This is a simple version that only uses a List<string> for the DropDownList. Using this example it would be trivial to use dictionaries and set the text/value properties:

View Model:

public class PhoneNumberViewModel
{
    // ... normal properties and attributes
    [DropDownList("ContactTypes")]  // <-- This is the important one!
    public string ContactType { get; set; }
    public static IEnumerable<string> ContactTypes
    {
        get
        {
            return new List<string>()
            {
                "Home", "Work", "Mobile"
            }
        }
    }
}

DropDownListAttribute Implementation

public class DropDownListAttribute : MetadataAttribute
{
    private string _items;

    public DropDownListAttribute(string items)
    {
        _items = items;
    }

    public override void Process(System.Web.Mvc.ModelMetadata modelMetaData)
    {
        modelMetaData.AdditionalValues.Add("Items", _items);
        modelMetaData.TemplateHint = TemplateHints.DropDownList; // Just a const string
    }
}

Custom HTML Helper used to render elements that need access to the view model instance

@Html.ContextualEditor(propertyName)

ContextualEditor HTML Helper Implementation

public static MvcHtmlString ContextualEditor(this HtmlHelper html, string expression)
{
    var model = html.ViewData.Model;
    var propertyMetadata = html.ViewData.ModelMetadata.Properties.Where(x => x.PropertyName == expression).Single();
    switch (propertyMetadata.TemplateHint)
    {
        case TemplateHints.DropDownList:
            var items = (IEnumerable<string>)model.GetType().GetProperty((string)propertyMetadata.AdditionalValues["Items"]).GetValue(model, null);
            var selectedValue = (string)model.GetType().GetProperty(expression).GetValue(model, null);
            var selectList = new TagBuilder("select");
            selectList.MergeAttribute("id", expression);
            selectList.MergeAttribute("name", expression);
            var options = new StringBuilder();
            foreach (var item in items)
            {
                var option = new TagBuilder("option");
                option.InnerHtml = item;
                if (item == selectedValue)
                    option.MergeAttribute("selected", "selected");
                options.Append(option.ToString());
            }
            selectList.InnerHtml = options.ToString();
            return MvcHtmlString.Create(selectList.ToString());
        default:
            return System.Web.Mvc.Html.EditorExtensions.Editor(html, expression);
    }
}

Please let me know what you think of the implementation. I'd be interested to know if anyone has found a more elegant way of achieving this result.

\$\endgroup\$
  • \$\begingroup\$ What exactly are you trying to do? Are you trying to display a drop-down list? \$\endgroup\$ – user12400 Apr 27 '12 at 0:54
3
\$\begingroup\$

Why not just do this?

public class PhoneNumberViewModel
{
    // ... normal properties and attributes
    [DropDownList("ContactTypes")]  // <-- This is the important one!
    public string ContactType { get; set; }

    public static IEnumerable<SelectListItem> ContactTypes
    {
        get { return new[] { "Home", "Work", "Mobile" }.ToSelectList(); }
    }
}

public static class ExtensionsForSelectListItem
{
    public static IEnumerable<SelectListItem> ToSelectList(this IEnumerable<string> items)
    {
        return items.Select(x => new SelectListItem { Value = x });
    }

    public static MvcHtmlString ContextualEditor(this HtmlHelper html, string expression)
    {

        var model = html.ViewData.Model;

        var propertyMetadata = ModelMetadata.FromStringExpression(expression, html.ViewData);


        switch (propertyMetadata.TemplateHint)
        {
            case TemplateHints.DropDownList:
                var items = (IEnumerable<SelectListItem>)model.GetType().GetProperty((string)propertyMetadata.AdditionalValues["Items"]).GetValue(model, null);
                return html.DropDownList(expression, items);
            default:
                return System.Web.Mvc.Html.EditorExtensions.Editor(html, expression);
        }
    }


}

I didn't test the code to get the items. But I'm assuming that you had that part working.

I also authored my own HTML Helper Library if you want check out some other examples.

https://bitbucket.org/grcodemonkey/buildmvc

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.