9

Lets start with a simple view model:

public class MyViewModel
{
  public string Value { get; set; }
  public IEnumerable<SelectListItem> Values { get; set; }
}

A drop down list might look like:

@Html.DropDownListFor(m => m.Value, Model.Values)

However, because a drop down list requires two values it can't be used like:

public class MyViewModel
{
  [UIHint("DropDownList")]
  public string Value { get; set; }
  public IEnumerable<SelectListItem> Values { get; set; }
}

with a view containing:

@Html.EditForModel()

Because there is no inherent way for the drop down to know the source, until you derrive from UIHint:

public DropDownListAttribute : UIHintAttribute
{
  public DropDownListAttribute (string valuesSource)
    : base("DropDownList", "MVC")
  {
  }
}

Then one might use it like:

public class MyViewModel
{
  [DropDownList("Planets")]
  public string PlanetId{ get; set; }
  public IEnumerable<SelectListItem> Planets { get; set; }

  [DropDownList("Cars")]
  public string CarId{ get; set; }
  public IEnumerable<SelectListItem> Cars { get; set; }
}

However, this isn't really strongly typed, someone renames one of the magic strings or propery names without changing the other and it breaks at run-time.

One theoretical solution is to create a generic attribute:

public DropDownListAttribute<TModel, TValue> : UIHintAttribute
{
  public DropDownListAttribute (Expression<Func<TModel, TValue> expression)
    : base("DropDownList", "MVC")
  {
  }
}

and the usage would be:

public class MyViewModel
{
  [DropDownList<MyViewModel, IEnumerable<SelectListItem>>( m => m.Planets)]
  public string PlanetId{ get; set; }
  public IEnumerable<SelectListItem> Planets { get; set; }
}

But (currently) Generic Attributes aren't allowed :/

Another option is to encapsulate the two into a single class that the ModelBinder can recreate on post-back:

public class DropDownListTemplate
{
  public string SelectedValue { get; set; }
  public IEnumerable<SelectListItem> Values { get; set; }
}

public class MyViewModel
{
  public DropDownListTemplate Planet { get; set; }
}

This creates simplicity in the ViewModel, Binding and EditFor/DisplayFor templates but from my limited knowledge of AutoMapper it adds complexity when AutoMapper Mapping to Properties of Class Properties. As far as I know I can't simply:

public class MyPlanets
{
  public string SelectedPlanet { get; set; }
}

Mapper.CreateMap<MyPlanets, MyViewModel>();
Mapper.CreateMap<MyViewModel, MyPlanets>();

Is there an easier way with automapper to map these values auto-magically or is there a way to create a strongly-typed non-generic attribute?

Community
  • 1
  • 1
Erik Philips
  • 53,428
  • 11
  • 128
  • 150
  • Even if you weren't using generics, I don't believe C# will let you use lambda expressions when decorating members with custom attributes. It's pretty much limited to literals and `typeof()` expressions. – StriplingWarrior May 13 '15 at 21:29
  • 1
    @StriplingWarrior oh good point! No `Expression`, `Func` etcs... yeesh. Maybe I can [dependency inject the drop down values directly into the attribute](http://stackoverflow.com/questions/4695902/asp-net-mvc-3-action-filters-and-autofac-dependency-injection).... :) – Erik Philips May 13 '15 at 21:33
  • @ErikPhilips you'd have to override the Attribute activator step though wouldn't you? Since your constructors would want the values when you decorated them. Maybe property injection inside of an attribute...tricky. You'd still need to override the attribute construction phase. – David L May 13 '15 at 21:36
  • 1
    On a separate note, I just found this interesting link http://stackoverflow.com/questions/19056573/authorize-attribute-lifecycle : ASP.NET MVC will cache ActionFilters and try to reuse them on subsequent requests. The actual authorization will occur on each request but the contructor will only get called on the first. You should not maintain any internal state in an ActionFilter. – David L May 13 '15 at 21:37
  • Yeah, I think anything you do to try to avoid magic strings is just going to cause more problems than anything. I've learned to accept the fact that MVC uses magic strings for some things. I haven't really had any big problems arise from it so far (crosses fingers). – StriplingWarrior May 13 '15 at 21:41
  • I'll have to do some testing, my link in the comment stated the same thing, but appeared to suggest that Autofac could either; override the caching or (much less likely) it could/would update the dependency per request. – Erik Philips May 13 '15 at 21:43
  • @ErikPhilips I'd imagine that the only way to override the caching would be to hijack the Attribute construction step in the lifecycle and explicitly tell it NOT to pull those values from cache. – David L May 13 '15 at 21:56

2 Answers2

1

To make the property name strongly typed you could use nameof which is introduced in C# 6.

nameof is an expression just like typeof, but instead of returning the value's type it returns the value as a string.

Used to obtain the simple (unqualified) string name of a variable, type, or member. When reporting errors in code, hooking up model-view-controller (MVC) links, firing property changed events, etc., you often want to capture the string name of a method. Using nameof helps keep your code valid when renaming definitions.

public class MyViewModel
{
  [DropDownList(nameof(Planets))]
  public string PlanetId{ get; set; }
  public IEnumerable<SelectListItem> Planets { get; set; }

  [DropDownList(nameof(Cars))]
  public string CarId{ get; set; }
  public IEnumerable<SelectListItem> Cars { get; set; }
}
Waaghals
  • 2,029
  • 16
  • 30
0

You could name your property in Planets as "PlanetSelectedValue" which would cause AutoMapper to automagically determine what value goes there. (i.e. Planet.SelectedValue maps to PlanetSelectedValue)

This is similar to the magic string problem in that if you change the property name, AutoMapper will no longer know where to put that value, but that is no different than any other AutoMapper issue.

public class MyPlanets
{
    public string PlanetSelectedValue { get; set; }
}

OR:

Use AutoMapper's IgnoreMap attribute to ignore the DropDownListTemplate property and make a separate AutoMapper-friendly PlanetId property that manipulates the values inside the DropdownListTemplate.

public class DropDownListTemplate
{
    public string SelectedValue { get; set; }
    public IEnumerable<SelectListItem> Values { get; set; }
}

public class MyViewModel
{
    [IgnoreMap]
    public DropDownListTemplate Planet { get; set; }
    public string PlanetId
    {
        get { return Planet.SelectedValue; }
        set { Planet.SelectedValue = value; }
    }
}

Also, when it is possible for your project, look at the C# 6.0 nameof() expression to pull in the name of a property without using the magic string.

Grax32
  • 3,986
  • 1
  • 17
  • 32