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?
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
otherwise you will throw this exception (
List<DisplayPersonViewModel>
is notIEnumerable<object>
orIPagedList<object>
, but it isobject
)Note that it is not clear if you want the
ModelMetadata
for thetype
in the collection or for each item in the collection, so I have included both, plus code that gets thetype
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 ofbool
values, and then use an indexer in the loop to check the value). A better alternative would be to have yourShowOnIndexView
attribute implementIMetadataAware
and add a value to theAdditionalValues
ofModelMetadata
so thatvar onIndex
code is not required at all. For an example of implementingIMetadataAware
, refer CustomAttribute reflects html attribute MVC5