Search code examples
asp.net-mvcdisplay-templates

ASP.NET MVC Display Template for strings is used for integers


I recently hit an issue with ASP.NET MVC display templates. Say this is my model:

public class Model
{
    public int ID { get; set; }
    public string Name { get; set; }
}

this is the controller:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View(new Model());
    }
}

and this is my view:

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<DisplayTemplateWoes.Models.Model>" %>

<!DOCTYPE html>

<html>
<head runat="server">
    <title>Index</title>
</head>
<body>
    <div>
        <%: Html.DisplayForModel() %>
    </div>
</body>
</html>

If I need for some reason a display template for all strings I will create a String.ascx partial view like this:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<string>" %>

<%: Model %> (<%: Model.Length %>)

And here is the problem - at runtime the following exception is thrown: "The model item passed into the dictionary is of type 'System.Int32', but this dictionary requires a model item of type 'System.String'"

It seems that String.ascx is used for both the integer and string property of the Model class. I expected it to be used only for the string property - after all it is named String.ascx not Object.ascx or Int32.ascx.

Is this by design? If yes - is it documented somewhere? If not - can it be considered a bug?


Solution

  • This seem to be by design. You will have to make string template more general. String template works as default template for every non-complex model that doesn't have it's own template.

    Default template for string (FormattedModelValue is object):

    internal static string StringTemplate(HtmlHelper html) {
        return html.Encode(html.ViewContext.ViewData.TemplateInfo.FormattedModelValue);
    }
    

    Template selection looks like this:

    foreach (string templateHint in templateHints.Where(s => !String.IsNullOrEmpty(s))) {
        yield return templateHint;
    }
    
    // We don't want to search for Nullable<T>, we want to search for T (which should handle both T and Nullable<T>)
    Type fieldType = Nullable.GetUnderlyingType(metadata.RealModelType) ?? metadata.RealModelType;
    
    // TODO: Make better string names for generic types
    yield return fieldType.Name;
    
    if (!metadata.IsComplexType) {
        yield return "String";
    }
    else if (fieldType.IsInterface) {
        if (typeof(IEnumerable).IsAssignableFrom(fieldType)) {
            yield return "Collection";
        }
    
        yield return "Object";
    }
    else {
        bool isEnumerable = typeof(IEnumerable).IsAssignableFrom(fieldType);
    
        while (true) {
            fieldType = fieldType.BaseType;
            if (fieldType == null)
                break;
    
            if (isEnumerable && fieldType == typeof(Object)) {
                yield return "Collection";
            }
    
            yield return fieldType.Name;
        }
    }
    

    So if you want to create template only for string, you should do it like this (String.ascx):

    <%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<object>" %>
    
    <% var model = Model as string; %>
    <% if (model != null) { %>
        <%: model %> (<%: model.Length %>)
    <% } else { %>
        <%: Model %>
    <% } %>