强类型通用属性替代方案

Strongly Typed Generic Attribute alternatives

让我们从一个简单的视图模型开始:

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

下拉列表可能如下所示:

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

但是,由于下拉列表需要两个值,因此不能像这样使用:

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

视图包含:

@Html.EditForModel()

因为在您从 UIHint 派生之前,下拉菜单没有固有的方式知道来源:

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

然后可以像这样使用它:

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; }
}

然而,这并不是真正的强类型,有人重命名 magic strings 或 属性 名称之一而不更改另一个,它在 运行 时中断。

一个理论上的解决方案是创建一个通用属性:

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

用法为:

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:/

另一种选择是将两者封装到一个 class 中,ModelBinder 可以在 post-back 上重新创建:

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

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

这在 ViewModel、Binding 和 EditFor/DisplayFor 模板中创建了简单性,但根据我对 AutoMapper 的有限了解,它在 AutoMapper Mapping to Properties of Class Properties 时增加了复杂性。据我所知我不能简单地:

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

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

是否有更简单的方法使用 automapper 自动神奇地映射这些值,或者是否有创建强类型非通用属性的方法?

您可以将 Planets 中的 属性 命名为 "PlanetSelectedValue",这会导致 AutoMapper 自动确定那里的值。 (即 Planet.SelectedValue 映射到 PlanetSelectedValue)

这与魔术字符串问题类似,如果您更改 属性 名称,AutoMapper 将不再知道将该值放在哪里,但这与任何其他 AutoMapper 问题没有什么不同。

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

或:

使用 AutoMapper 的 IgnoreMap 属性忽略 DropDownListTemplate 属性 并制作一个单独的 AutoMapper 友好的 PlanetId 属性 来操纵 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; }
    }
}

此外,如果您的项目可行,请查看 C# 6.0 nameof() 表达式以在不使用魔术字符串的情况下提取 属性 的名称。

要使 属性 名称成为强类型,您可以使用 C# 6 中引入的 nameof

nameof 是一个类似于 typeof 的表达式,但不是返回值的类型,而是 returns 作为字符串的值。

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; }
}