我正在处理的一个项目的主要功能之一是用户能够基于预先存在的字段类型(例如众所周知的类型)配置表单(如在“表单”中填写) “用户名”、“出生日期”等,还有“通用类型”,如“字符串”、“日期时间”等)。
我们曾经有一个静态 ViewModel,它适用于“众所周知的”类型,看起来像这样:
public class UserInputModel
{
[StringLength(200)]
public string Name { get; set; }
[Required(ErrorMessageResourceName = "BirthDateEmptyError", ErrorMessageResourceType = typeof(Resources.ErrorMessages))]
public DateTime BirthDate { get; set; }
//Here comes a lot of other properties
}
列出了所有已知的属性,我们根据上下文显示或隐藏它们。
但是最后一个要求来了,改变了这一切。用户现在可以根据需要添加任意数量的泛型类型字段。为了做到这一点,我们决定让这个 InputModel 完全动态化。现在看起来像这样:
public class UserInputModel
{
// Each ModelProperty has an "Id" and a "Value" property
public ICollection<ModelProperty> Properties { get; set; }
}
这就像一个魅力。razor 视图只需要遍历集合,为集合的每个属性创建相应的控件,而不是标准的方式:
@Html.TextBoxFor(m => m.Properties[index].Value);
...我们很好地将数据作为填充表格返回。
=> 这很好用,但我们没有任何客户端验证。为此,我们需要一些元数据......我们不再通过注释获得,因为我们正在动态创建模型。
为了提供这些元数据,我创建了一个CustomModelMetadataProvider
继承自Global.asaxDataAnnotationsModelMetadataProvider
并将其注册为新的。ModelMetadataProvider
该CreateMetadata()
函数在创建 ViewModel 时被调用,并且对于我的 ViewModel 的每个属性......到目前为止都很好。
问题从哪里开始:为了向当前属性添加一些元数据,我首先需要确定我当前正在查看的属性(“名称”的最大长度为 200,“出生日期”没有,所以我无法分配默认情况下每个属性的最大长度)。不知何故,我还没有设法做到这一点,因为所有属性都具有相同的名称Value
和相同的容器类型ModelProperty
。
我尝试通过反射访问属性的容器,但由于 ModelAccessor 的目标是 ViewModel 本身(因为 lambda 表达式m => m.Properties
),以下构造为我提供了整个 ViewModel,而不仅仅是 ModelProperty:
var container = modelAccessor.Target.GetType().GetField("container");
var containerObject = (UserInputModel)container.GetValue(modelAccessor.Target);
我一直在翻来覆去,但找不到一种方法来识别我手头的 ModelProperty。有没有办法做到这一点?
更新:在所有可能的方向翻转了一段时间后,我们终于走了另一条路。我们基本上使用不显眼的 javascript 来使用 MVC 的验证功能,而无需触及属性或元数据。简而言之,我们将 HTML 属性value-data="true"
(以及所有其他必需的属性)添加到@Html.TextBoxFor()
语句中。这对于所有原子验证(必需、字符串长度等)都非常有效。