简体   繁体   中英

ASP.NET MVC Core DisplayFor not rendering correct type when enumerating polymorphic IEnumerable<T>

I am have a view model which has a property

public IRichTextContent Body { get; set; }

This interface inherits IEnumerable<IRichTextBlock> and there are 3 interfaces that inherit IRichTextBlock : IHtmlContent , IInlineImage and IInlineContentItem . These are all part of the Kentico Kontent Delivery .NET SDK . The normal recommended rendering approach for this property is:

@Html.DisplayFor(vm => vm.Body)

and everything works fine. For the IInlineImage and IHtmlContent types, with no display templates in the solution, ASP.NET MVC calls the ToString() method on them. If I place display templates for the types in the solution then these are picked up and used. The IInlineContentItem has a property of type object that can hold various actual types and ASP.NET MVC correctly resolves the right display template for this object, presumably due to the IEnumerable<object> implementation ( see the InlineContentItem ). Happy days so far, the meta data template resolution pixie magic works.

In some scenarios I want to be able to use different display templates, so a single display template for the type will not work. As the model property is a collection of different types I can't do this as it stands. So I figured I would enumerate the IEnumerable<IRichTextBlock> and then call DisplayFor() on the types passing a template where required. Something like this:

@foreach (var block in Model.Body)
{
    @switch (block)
    {
        case Kentico.Kontent.Delivery.Abstractions.IInlineImage image:
            @Html.DisplayFor(vm => image, "AmpInlineImage")
            break;
        default:
            @Html.DisplayFor(vm => block)
            break;
    }
}

For the case where I specify the template this works fine, the correct type is sent to the template. However, the default switch case without a template now does not resolve either the underlying type ToString() or the display templates in my solution. Instead it seems the default ASP.NET MVC object template is used for the IHtmlContent and nothing is rendered for the IInlineContentItem .

What is the difference here between the case where ASP.NET MVC correctly resolves the underlying types when enumerating the collection itself and the case where I am doing this? People do not normally seem to have issues with a foreach over a collection, but I presume the issue here is the polymorphism?

Your presumption is correct: based on the ASP.NET Core MVC source, the difference is the polymorphism, or specifically that template resolution does not handle inheritance of interface types. Here is an abridged summary of the method that finds the template name from the type :

public static IEnumerable<string> GetTypeNames(ModelMetadata modelMetadata, Type fieldType)
        {
            // ...
            var fieldTypeInfo = fieldType.GetTypeInfo();

            if (typeof(IEnumerable<IFormFile>) != fieldType)
            {
                yield return fieldType.Name;
            }

            if (fieldType == typeof(string))
            {
                // ...
            }
            else if (!modelMetadata.IsComplexType)
            {
                // A complex type is defined as a Type without a
                // TypeConverter that can convert from string
            }
            else if (!fieldTypeInfo.IsInterface)
            {
               var type = fieldType;
                while (true)
                {
                    type = type.GetTypeInfo().BaseType;
                    if (type == null || type == typeof(object))
                    {
                        break;
                    }

                    yield return type.Name;
                }
            }

            if (typeof(IEnumerable).IsAssignableFrom(fieldType))
            {
                if (typeof(IEnumerable<IFormFile>).IsAssignableFrom(fieldType))
                {
                    // ...
                }

                yield return "Collection";
            }
            else if (typeof(IFormFile) != fieldType && typeof(IFormFile).IsAssignableFrom(fieldType))
            {
                yield return nameof(IFormFile);
            }

            yield return "Object";
        }

Note how:

  1. The runtime type name is returned except for one special scenario.
  2. There is a condition when the type is not an interface that returns the type names in the hierarchy.
  3. Nothing else until the end where "Object" is returned as a generic template name.

This occurs regardless of the Kentico Kontent Delivery .NET SDK, and you can test it by creating a model property using an IEnumerable of a simple interface and setting it to a List of objects of a type implementing an interface that inherits that interface . If you do the foreach and @Html.DisplayFor on each item, the generic Object template is used.

In this case, you have some options:

  • Always pass in a template name (even create an extension method to automatically pull it from the runtime type).
  • Implement the IRichTextBlock.cshtml template.
  • Implement the Object.cshtml template.

An example of IRichTextBlock.cshtml is this:

@model Kentico.Kontent.Delivery.Abstractions.IRichTextBlock

@switch (Model)
{
  case Kentico.Kontent.Delivery.Abstractions.IInlineContentItem inlineContentItem:
    // Render inlineContentItem
    break;
  default:
    @Html.Raw(Model.ToString())
    break;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM