3

I have a project written in C# on the top on ASP.NET MVC 5 framework. I am trying to decouple my views from my view model so I can make my views reusable. With the heavy use of EditorTemplates I am able to create all of my standard views (i.e create, edit and details) by evaluating the ModelMetadata and the data-annotation-attributes for each property on the model, then render the page. The only thing that I am puzzled with is how to render the Index view.

My index view typically accepts an IEnumerable<object> or IPagedList<object> collection. In my view, I want to be able to evaluate the ModelMetadata of a each object/record in the collection to determine if a property on the object should be displayed or not.

In another words my view-model will look something like this

public class DisplayPersonViewModel
{
    public int Id{ get; set; }

    [ShowOnIndexView]
    public string FirstName { get; set; }

    [ShowOnIndexView]
    public string LastName { get; set; }

    [ShowOnIndexView]
    public int? Age { get; set; }

    public string Gender { get; set; }
}

Then my Index.cshtml view will accepts IPagedList<DisplayPersonViewModel> for each record in the collection, I want to display the value of the property that is decorated with ShowOnIndexView attribute.

Typically I would be able to do that my evaluating the ModelMetadata in my view with something like this

@model IPagedList<object>
@{
var elements = ViewData.ModelMetadata.Properties.Where(metadata => !metadata.IsComplexType && !ViewData.TemplateInfo.Visited(metadata))
                                                .OrderBy(x => x.Order)
                                                .ToList();
}

<tr>
    @foreach(var element in elements)
    {
        var onIndex = element.ContainerType.GetProperty(element.PropertyName)
                             .GetCustomAttributes(typeof(ShowOnIndexView), true)
                             .Select(x => x as ShowOnIndexView)
                             .FirstOrDefault(x => x != null);
        if(onIndex == null) 
        {
            continue;
        }

        @Html.Editor(element.PropertyName, "ReadOnly")
    }
</tr>

Then my controller will look something like this

public class PersonController : Controller
{
        public ActionResult Index()
        {
            // This would be a call to a service to give me a collection with items. but I am but showing the I would pass a collection to my view
            var viewModel = new List<DisplayPersonViewModel>();

            return View(viewModel);
        }
}

However the problem with evaluating ModelMetadata for the IPagedList<DisplayPersonViewModel> is that it gives me information about the collection itself not about the generic type or the single model in the collection. In another words, I get info like, total-pages, items-per-page, total-records....

Question

How can I access the ModelMetadata info for each row in the collection to be able to know which property to display and which not to?

Junior
  • 11,602
  • 27
  • 106
  • 212
  • What you doing is a bad idea for many reasons, including making your code difficult to unit test. That code belongs in a custom `HtmlHelper` method, not the view. And you have noted that your want to _display the value_, in which case why do you have an `EditorTemplate` (as opposed to a `DisplayTemplate`) –  Aug 04 '18 at 02:23
  • @StephenMuecke Helper it is. But how can write the helper and access my model rows? – Junior Aug 04 '18 at 03:11
  • That depends on how much functionality you want - and if you want to take it to max, you might be interested in [this article](https://www.codeproject.com/Articles/774228/MVC-Html-Table-Helper-Part-Display-Tables) to give you and indication of what is possible. Its pretty old now, and the code has been developed a lot since then and is now on [GitHub](https://github.com/stephenmuecke/mvc-tablehelper) - but unfortunately I have not got around to writing the docs for it yet –  Aug 04 '18 at 03:29

1 Answers1

2

I will preface this answer by recommending you do not pollute your view with this type of code. A far better solution would be to create a custom HtmlHelper extension method to generate the html, which gives you far more flexibility, can be unit tested, and is reusable.

The first thing you will need to change is the model declaration in the view which needs to be

@model object

otherwise you will throw this exception (List<DisplayPersonViewModel> is not IEnumerable<object> or IPagedList<object>, but it is object)

Note that it is not clear if you want the ModelMetadata for the type in the collection or for each item in the collection, so I have included both, plus code that gets the type

@model object
@{
    var elements = ViewData.ModelMetadata.Properties.Where(metadata => !metadata.IsComplexType && !ViewData.TemplateInfo.Visited(metadata)).OrderBy(x => x.Order).ToList();

    // Get the metadata for the model
    var collectionMetaData = ViewData.ModelMetadata;
    // Get the collection type
    Type type = collectionMetaData.Model.GetType();
    // Validate its a collection and get the type in the collection
    if (type.IsGenericType)
    {
        type = type.GetInterfaces().Where(t => t.IsGenericType)
            .Where(t => t.GetGenericTypeDefinition() == typeof(IEnumerable<>))
            .Single().GetGenericArguments()[0];
    }
    else if (type.IsArray)
    {
        type = type.GetElementType();
    }
    else
    {
        // its not a valid model
    }
    // Get the metadata for the type in the collection
    ModelMetadata typeMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, type);
}
....
@foreach (var element in collectionMetaData.Model as IEnumerable))
{
    // Get the metadata for the element
    ModelMetadata elementMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => element, type);
    ....

Note that using reflection to determine if the attribute exists in each iteration of your loop is inefficient, and I suggest you do that once (based on the typeMetadata). You could then (for example) generate an array of bool values, and then use an indexer in the loop to check the value). A better alternative would be to have your ShowOnIndexView attribute implement IMetadataAware and add a value to the AdditionalValues of ModelMetadata so that var onIndex code is not required at all. For an example of implementing IMetadataAware, refer CustomAttribute reflects html attribute MVC5

  • Thank you! I tried to add the `IMetadataAware` to my attributes so I can add additional-hints to avoid having the need to use `GetCustomAttributes` to find the attributes. But I do not get the additional values as expected. However, when I use `GetCustomAttributes` then the attribute will have the additional values. For example, in the code below, `onIndex` has additional-values where `element` does not. `var onIndex = element.ContainerType.GetProperty(element.PropertyName).GetCustomAttributes(typeof(ShowOnIndexView), true).Select(x => x as ShowOnIndexView).FirstOrDefault(x => x != null);` – Junior Aug 06 '18 at 19:18
  • @MikeA. I have created [this Gist](https://gist.github.com/stephenmuecke/b4c1d4e71fe8aca23c1bee28f7ff9b60) to show you how to create the attribute, apply it to your model properties and use it in the view (on reflection, I would probably make it an `[ExcludeFromIndexView]` instead, assuming most properties will be displayed in the view) –  Aug 07 '18 at 06:28
  • Thanks a lot! There seems to be two different contracts with the name `IMetadataAware` `System.Web.ModelBinding.IMetadataAware` and `System.Web.Mvc.IMetadataAware` . I was using the wrong one. The correct one to use is `System.Web.Mvc.IMetadataAware` – Junior Aug 07 '18 at 16:27