6

Problem Description

My problem is similar to this question but instead of applying Data Annotations to the property Name via reflection (handled by ModelMetadata.DisplayName) I am applying them to the value (not handled by ModelMetadata).

Detailed Description

In the context of an ASP.NET MVC program.
Suppose I have a Model class

public class Model
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string NickName { get; set; }
    public string Address { get; set; }
    public int Phone { get; set; }

    [Display(Name = "Start Date")]
    [DataType(DataType.Date)]
    [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:dd/MM/yyyy}")]
    public DateTime StartDate { get; set }

    [Display(Name = "End Date")]
    [DataType(DataType.Date)]
    [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:dd/MM/yyyy}")]
    public DateTime EndDate { get; set }
}

Then suppose this Model is used in at least 5 different views where the value of every property must be displayed in full. (Sometimes for every instance, other times for a handful, other times for one).

I can manually list each Property access within it's own
<td>@Html.DisplayFor(item.<Property>)</td>
for every view.

But that won't help me if later on the definition for Model expands to include new Properties (like Description and Relation and Reliability). Then I'd need to manually update every occurrence of the complete Model listing.

I can use reflection to iterate over a list of PropertyInfo's and save having to manually list each property by using
<td>@property.GetValue(item)</td>

But DisplayFor(x) does not support an expression as complex as x=>property.GetValue(item), and this means I lose the Data Annotations that format my DateTime as
01/01/1990
instead of
01-Jan-90 12:00:00 AM
and would likely also result in the loss of all types of annotation including validation.

Problem Solutions

So far I have considered (and in some cases attempted) the following solutions:

  • [Failed] Manually craft an Expression which emulates the functionality of @property.GetValue(item)
    [Edit]
  • [Failed] Pass DisplayFor a MethodInfo object representing the Property Accessor DisplayFor(x => property.GetGetMethod()) as well as .Invokeing it on x.
    [/Edit]
  • Retrieve the value manually as normal, and
    • execute a method on it to manually retrieve and implement Annotation Data on it prior to insertion in the view element as this question suggests, or
    • Re-implement the DisplayFor handling of Data Annotations on an as-needed basis in a Display Template View and apply that directly to the value via DisplayFor as this question suggested
  • Refactor the Model class to contain only a list(SortedList?) of 'Prop' instances, where 'Prop' is a class representing a Property with a Name and Value element.

This last solution would turn the broken
@Html.DisplayFor(m=>property.GetValue(item)
into a theoretically working
@Html.DisplayFor(m=>item.Properties[i].Value)
Which aside from the slightly unintuitive need for getting a property called Name (Properties["Name"]) by (.Value), seems the most workable solution, at the cost of Model clarity.

[Edit]
Most recently I have created a Utility method which retrieves the DisplayFormatAttribute from the PropertyInfo and returns either the DisplayFormatString or the default of "{0}" if a format string was not annotated. I have then used it to create a collection of preformatted property values within a ViewModel. This seems for now, to be the most elegant way I know of to decouple the View from the Model as much as possible while still retrieving the necessary data from it.
[/Edit]

The Question

This is at the moment, purely a learning exercise, but I would like to know...
Is it possible to succeed where I have failed and both have my Reflection cake and eat the Data Annotations too? Or must I seek an alternative solution?
If I must seek an alternative solution, are there routes I have missed, or am I at least on the right track?

Community
  • 1
  • 1
Robere Starkk
  • 91
  • 1
  • 6
  • Decorate your models with custom attributes and write code for how to render each one. Why are you trying to do this – CodingYoshi May 09 '17 at 02:15
  • As I said, it's purely an educational exercise. I was building a simple project from a tutorial to familiarise myself with the ASP.NET version of how MVC works and thought to myself "If the whole point of using MVC is to separate the concerns, why must the views have intimate knowledge of every single property name on the model when conceptually all they want to do is 'Show values of all, or some generated subset of, the properties'?" So I decided I'd see if I could do just that. – Robere Starkk May 10 '17 at 23:18
  • Two of my suggested solutions are actually to implement the formatting myself in various ways, (though using custom attributes would have the same problem as Data Annotations in that they are not readily accessible via PropertyInfo)... but I'd rather avoid reinventing the wheel in that sense if possible, since the Html webhelper templates have defined all of that functionality already for direct property accessing. – Robere Starkk May 10 '17 at 23:20
  • It doesn't have intimate knowledge-just reads from the model. Of course there will always be some coupling otherwise classes will not be very useful on their own. The idea is to minimize coupling, not remove it altogether. But good for you for thinking this way. – CodingYoshi May 11 '17 at 00:03
  • Thank you. Minimising the coupling is exactly what I was trying to achieve by replacing explicit name referencing with iterating over a collection of them created by logic elsewhere. – Robere Starkk May 11 '17 at 01:42
  • Unfortunately, the best solution I've come up with so far requires me to manually apply the formatting. Which isn't a big deal, except it will also require me to manually implement all the other attributes as I start to use them, in particular Validation HTML. – Robere Starkk May 11 '17 at 01:44

2 Answers2

2

Maybe something similar to:

@foreach (var property in Model.GetType().GetProperties())
{
    <li>@property.GetValue(Model, null)</li>
}
Cristian Szpisjak
  • 2,429
  • 2
  • 19
  • 32
  • Aside from adding the `null` parameter (which as far as I can see hasn't changed anything), that's precisely what I've been using, but it still has the problem of not applying the Data Annotation Formatting when accessed that way. – Robere Starkk May 10 '17 at 23:15
0

Great Success

Revisiting my original attempt to manually craft the expression dynamically, I discovered this article which did precisely what I wanted to do, and using mostly Microsoft provided code as well!

Though the Microsoft code was difficult to find (the link in the article is broken and the example slightly outdated for the code I did find), I was able to use it to good effect to implement my own DisplayFor extension method.
Unfortunately, due to my model being a list rather than a single instance, I still needed to create a partial view to pass an instance to, so that I could access the properties via Model from within the generated expression.

My View code now looks like this:

@foreach (var thing in Model.CollectionOfThings)
{
    <tr>
        @foreach (var prop in typeof(Thing).GetProperties())
        {
            <td>
                @{
                    Html.RenderPartial("~/Views/Shared/_DisplayForReflectedProperty.cshtml", 
                    new Tuple<Thing, PropertyInfo>(thing, prop));
                }
            </td>
        }
}

With _DisplayForReflectedProperty as simple as

@using WebApplication1.Models
@using System.Reflection
@using WebApplication1.Extensions

@model Tuple<Thing, PropertyInfo>

@Html.DisplayFor("Item1."+Model.Item2.Name)

And the only difference between my DisplayFor extension and the one in the article is the null object parameter in this function call (plus the obvious conversions from EditorFor to DisplayFor):

var lambda = System.Linq.Dynamic.DynamicExpression.ParseLambda(typeof(TModel), 
             null, expression);

Using this template I can now generate arbitrary (from code perspective, specific from business rule perspective) subsets of a model's properties to display in any manner I wish, without having to custom tailor my views to each particular subset, while retaining all the benefits of using the 'For' Helpers!

Robere Starkk
  • 91
  • 1
  • 6