Yus*_*zun 16 asp.net-mvc model-binding data-annotations razor asp.net-mvc-5
有没有办法将视图模型属性作为html端具有不同名称和id值的元素进行反射.
这是我想要实现的主要问题.所以问题的基本介绍如下:
1-我有一个视图模型(作为示例),它为视图侧的过滤操作创建.
public class FilterViewModel
{
public string FilterParameter { get; set; }
}
Run Code Online (Sandbox Code Playgroud)
2-我有一个控制器动作,它是为GETting表格值创建的(这里是过滤器)
public ActionResult Index(FilterViewModel filter)
{
return View();
}
Run Code Online (Sandbox Code Playgroud)
3-我认为用户可以过滤某些数据,并通过表单提交通过查询字符串发送参数.
@using (Html.BeginForm("Index", "Demo", FormMethod.Get))
{
@Html.LabelFor(model => model.FilterParameter)
@Html.EditorFor(model => model.FilterParameter)
<input type="submit" value="Do Filter" />
}
Run Code Online (Sandbox Code Playgroud)
4-我想在渲染视图输出中看到的是
<form action="/Demo" method="get">
<label for="fp">FilterParameter</label>
<input id="fp" name="fp" type="text" />
<input type="submit" value="Do Filter" />
</form>
Run Code Online (Sandbox Code Playgroud)
5-作为解决方案,我想修改我的视图模型,如下所示:
public class FilterViewModel
{
[BindParameter("fp")]
[BindParameter("filter")] // this one extra alias
[BindParameter("param")] //this one extra alias
public string FilterParameter { get; set; }
}
Run Code Online (Sandbox Code Playgroud)
所以基本问题是关于BindAttribute但使用复杂类型属性.但是,如果有一种内置的方式,这样做要好得多.内置专业人士:
1-使用TextBoxFor,EditorFor,LabelFor和其他强类型视图模型助手可以更好地理解和交流彼此.
2- Url路由支持
3-没有问题的框架:
一般来说,我们建议人们不要编写自定义模型绑定器,因为它们很难正确,很少需要它们.我在这篇文章中讨论的问题可能是其中一个需要保证的情况.
经过一些研究,我发现了这些有用的作品:
结果:但他们都没有给我我的问题确切的解决方案.我正在寻找一个针对这个问题的强类型解决方案.当然,如果您知道其他任何方式,请分享.
更新
我想要这样做的根本原因基本上是:
1-每次我想更改html控件名称,然后我必须在编译时更改PropertyName.(在代码中更改字符串之间更改属性名称存在差异)
2-我想隐藏(伪装)最终用户的不动产名称.大多数情况下,View Model属性名称与映射的Entity Objects属性名称相同.(出于开发人员的可读性原因)
3-我不想删除开发人员的可读性.想想很多具有2-3个字符长度和mo含义的属性.
4-有很多视图模型被编写.所以改变他们的名字将比这个解决方案花费更多的时间.
5-这将是比其他问题更好的解决方案(在我的POV中),直到现在.
实际上,有一种方法可以做到这一点.
在ASP.NET中绑定的元数据TypeDescriptor
,而不是通过直接反射.为了更加珍贵,AssociatedMetadataTypeTypeDescriptionProvider
使用,反过来,TypeDescriptor.GetProvider
我们只使用我们的模型类型作为参数调用:
public AssociatedMetadataTypeTypeDescriptionProvider(Type type)
: base(TypeDescriptor.GetProvider(type))
{
}
Run Code Online (Sandbox Code Playgroud)
所以,我们需要的是TypeDescriptionProvider
为我们的模型设置我们的自定义.
让我们实现我们的自定义提供程序.首先,让我们定义自定义属性名称的属性:
[AttributeUsage(AttributeTargets.Property)]
public class CustomBindingNameAttribute : Attribute
{
public CustomBindingNameAttribute(string propertyName)
{
this.PropertyName = propertyName;
}
public string PropertyName { get; private set; }
}
Run Code Online (Sandbox Code Playgroud)
如果您已具有所需名称的属性,则可以重复使用它.上面定义的属性只是一个例子.我更喜欢使用,JsonPropertyAttribute
因为在大多数情况下我使用json和Newtonsoft的库并且只想定义一次自定义名称.
下一步是定义自定义类型描述符.我们不会实现整个类型描述符逻辑并使用默认实现.仅覆盖属性访问:
public class MyTypeDescription : CustomTypeDescriptor
{
public MyTypeDescription(ICustomTypeDescriptor parent)
: base(parent)
{
}
public override PropertyDescriptorCollection GetProperties()
{
return Wrap(base.GetProperties());
}
public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
{
return Wrap(base.GetProperties(attributes));
}
private static PropertyDescriptorCollection Wrap(PropertyDescriptorCollection src)
{
var wrapped = src.Cast<PropertyDescriptor>()
.Select(pd => (PropertyDescriptor)new MyPropertyDescriptor(pd))
.ToArray();
return new PropertyDescriptorCollection(wrapped);
}
}
Run Code Online (Sandbox Code Playgroud)
还需要实现自定义属性描述符.同样,除属性名称之外的所有内容都将由默认描述符处理.注意,NameHashCode
由于某种原因是一个单独的属性.随着名称的改变,所以它的哈希码也需要改变:
public class MyPropertyDescriptor : PropertyDescriptor
{
private readonly PropertyDescriptor _descr;
private readonly string _name;
public MyPropertyDescriptor(PropertyDescriptor descr)
: base(descr)
{
this._descr = descr;
var customBindingName = this._descr.Attributes[typeof(CustomBindingNameAttribute)] as CustomBindingNameAttribute;
this._name = customBindingName != null ? customBindingName.PropertyName : this._descr.Name;
}
public override string Name
{
get { return this._name; }
}
protected override int NameHashCode
{
get { return this.Name.GetHashCode(); }
}
public override bool CanResetValue(object component)
{
return this._descr.CanResetValue(component);
}
public override object GetValue(object component)
{
return this._descr.GetValue(component);
}
public override void ResetValue(object component)
{
this._descr.ResetValue(component);
}
public override void SetValue(object component, object value)
{
this._descr.SetValue(component, value);
}
public override bool ShouldSerializeValue(object component)
{
return this._descr.ShouldSerializeValue(component);
}
public override Type ComponentType
{
get { return this._descr.ComponentType; }
}
public override bool IsReadOnly
{
get { return this._descr.IsReadOnly; }
}
public override Type PropertyType
{
get { return this._descr.PropertyType; }
}
}
Run Code Online (Sandbox Code Playgroud)
最后,我们需要我们的自定义TypeDescriptionProvider
和方法将其绑定到我们的模型类型.默认情况下,TypeDescriptionProviderAttribute
旨在执行该绑定.但在这种情况下,我们无法获得我们想要在内部使用的默认提供程序.在大多数情况下,默认提供商将是ReflectTypeDescriptionProvider
.但这并不能保证,并且由于它的保护级别,这个提供商是无法访问的 - 它是internal
.但我们仍然希望回退到默认提供商.
TypeDescriptor
还允许通过AddProvider
方法为我们的类型显式添加提供程序.这就是我们将要使用的.但首先,让我们定义我们的自定义提供者本身:
public class MyTypeDescriptionProvider : TypeDescriptionProvider
{
private readonly TypeDescriptionProvider _defaultProvider;
public MyTypeDescriptionProvider(TypeDescriptionProvider defaultProvider)
{
this._defaultProvider = defaultProvider;
}
public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
{
return new MyTypeDescription(this._defaultProvider.GetTypeDescriptor(objectType, instance));
}
}
Run Code Online (Sandbox Code Playgroud)
最后一步是将我们的提供程序绑定到我们的模型类型.我们可以以任何我们想要的方式实现它.例如,让我们定义一些简单的类,例如:
public static class TypeDescriptorsConfig
{
public static void InitializeCustomTypeDescriptorProvider()
{
// Assume, this code and all models are in one assembly
var types = Assembly.GetExecutingAssembly().GetTypes()
.Where(t => t.GetProperties().Any(p => p.IsDefined(typeof(CustomBindingNameAttribute))));
foreach (var type in types)
{
var defaultProvider = TypeDescriptor.GetProvider(type);
TypeDescriptor.AddProvider(new MyTypeDescriptionProvider(defaultProvider), type);
}
}
}
Run Code Online (Sandbox Code Playgroud)
并通过Web激活调用该代码:
[assembly: PreApplicationStartMethod(typeof(TypeDescriptorsConfig), "InitializeCustomTypeDescriptorProvider")]
Run Code Online (Sandbox Code Playgroud)
或者只是在Application_Start
方法中调用它:
public class MvcApplication : HttpApplication
{
protected void Application_Start()
{
TypeDescriptorsConfig.InitializeCustomTypeDescriptorProvider();
// rest of init code ...
}
}
Run Code Online (Sandbox Code Playgroud)
但这不是故事的结局.:(
考虑以下模型:
public class TestModel
{
[CustomBindingName("actual_name")]
[DisplayName("Yay!")]
public string TestProperty { get; set; }
}
Run Code Online (Sandbox Code Playgroud)
如果我们尝试写.cshtml
视图如下:
@model Some.Namespace.TestModel
@Html.DisplayNameFor(x => x.TestProperty) @* fail *@
Run Code Online (Sandbox Code Playgroud)
我们会得到ArgumentException
:
System.Web.Mvc.dll中出现"System.ArgumentException"类型的异常,但未在用户代码中处理
其他信息:找不到属性Some.Namespace.TestModel.TestProperty.
因为所有帮助者很快或后来都会调用ModelMetadata.FromLambdaExpression
方法.这个方法接受我们提供的表达式(x => x.TestProperty
)并直接从成员信息中获取成员名称,并且不知道我们的任何属性,元数据(谁在乎,呵呵?):
internal static ModelMetadata FromLambdaExpression<TParameter, TValue>(/* ... */)
{
// ...
case ExpressionType.MemberAccess:
MemberExpression memberExpression = (MemberExpression) expression.Body;
propertyName = memberExpression.Member is PropertyInfo ? memberExpression.Member.Name : (string) null;
// I want to cry here - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// ...
}
Run Code Online (Sandbox Code Playgroud)
对于x => x.TestProperty
(这里x
是TestModel
)这个方法将返回TestProperty
,而不是actual_name
,但模型元数据包含actual_name
属性,没有TestProperty
.这就是the property could not be found
抛出错误的原因.
这是设计失败.
尽管如此不便,但有几种解决方法,例如:
最简单的方法是通过他们重新定义的名称访问我们的成员:
@model Some.Namespace.TestModel
@Html.DisplayName("actual_name") @* this will render "Yay!" *@
Run Code Online (Sandbox Code Playgroud)
这个不好.根本没有intellisense,因为我们的模型更改,我们将没有任何编译错误.任何改变都可以打破,没有简单的方法来检测它.
另一种方法有点复杂 - 我们可以创建自己的助手版本,并禁止任何人调用默认助手或 ModelMetadata.FromLambdaExpression
具有重命名属性的模型类.
最后,首选两者的组合:编写自己的模拟以获取具有重定义支持的属性名称,然后将其传递给默认帮助程序.像这样的东西:
@model Some.Namespace.TestModel
@Html.DisplayName(Html.For(x => x.TestProperty))
Run Code Online (Sandbox Code Playgroud)
编译时和智能感知支持,无需花费大量时间来完成整套帮助.利润!
此外,上面描述的所有内容都像模型绑定的魅力.在模型绑定过程中,默认绑定器也使用收集的元数据TypeDescriptor
.
但我想绑定json数据是最好的用例.您知道,许多Web软件和标准都使用lowercase_separated_by_underscores
命名约定.不幸的是,这不是C#的通常惯例.拥有以不同惯例命名的成员的课程看起来很丑陋,最终可能会遇到麻烦.特别是当你有工具每次抱怨命名违规时.
ASP.NET MVC默认模型绑定器不会像调用newtonsoft JsonConverter.DeserializeObject
方法时那样将json绑定到模型.相反,json解析为字典.例如:
{
complex: {
text: "blabla",
value: 12.34
},
num: 1
}
Run Code Online (Sandbox Code Playgroud)
将被翻译成以下字典:
{ "complex.text", "blabla" }
{ "complex.value", "12.34" }
{ "num", "1" }
Run Code Online (Sandbox Code Playgroud)
稍后,这些值以及来自查询字符串,路由数据等的其他值,由不同的实现收集IValueProvider
,将由默认绑定器用于通过收集的元数据绑定模型TypeDescriptor
.
因此,我们从创建模型,渲染,绑定它并使用它来完全循环.
简短的答案是否定的,而答案仍然很长.没有内置的帮助器,属性,模型绑定器,无论它是什么(没有任何开箱即用).
但是我在回答之前所做的(我删除它)是我昨天意识到的一个糟糕的解决方案.我打算把它放在github中,因为谁还想看(也许它解决了某些问题)(我也不建议它!)
现在我再次搜索它,我找不到任何有用的东西.如果您使用类似AutoMapper或ValueInjecter之类的工具来将ViewModel对象映射到Business对象,并且如果您想混淆View Model参数,那么可能您遇到了麻烦.当然你可以做到这一点,但强类型的html助手不会帮助你.我甚至不讨论是否有其他开发人员在分支和工作于常见的视图模型.
幸运的是我的项目(4个人正在处理它,以及它的商业用途)现在还不是那么大,所以我决定更改View Model属性名称!(还有很多工作要做.数以百计的视图模型来混淆他们的属性!!!)谢谢Asp.Net MVC!
在我提出的链接中有一些方法.但是如果您仍想使用BindAlias属性,我只建议您使用以下扩展方法.至少你不必编写你在BindAlias属性中编写的相同别名字符串.
这里是:
public static string AliasNameFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression)
{
var memberExpression = ExpressionHelpers.GetMemberExpression(expression);
if (memberExpression == null)
throw new InvalidOperationException("Expression must be a member expression");
var aliasAttr = memberExpression.Member.GetAttribute<BindAliasAttribute>();
if (aliasAttr != null)
{
return MvcHtmlString.Create(aliasAttr.Alias).ToHtmlString();
}
return htmlHelper.NameFor(expression).ToHtmlString();
}
public static string AliasIdFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression)
{
var memberExpression = ExpressionHelpers.GetMemberExpression(expression);
if (memberExpression == null)
throw new InvalidOperationException("Expression must be a member expression");
var aliasAttr = memberExpression.Member.GetAttribute<BindAliasAttribute>();
if (aliasAttr != null)
{
return MvcHtmlString.Create(TagBuilder.CreateSanitizedId(aliasAttr.Alias)).ToHtmlString();
}
return htmlHelper.IdFor(expression).ToHtmlString();
}
public static T GetAttribute<T>(this ICustomAttributeProvider provider)
where T : Attribute
{
var attributes = provider.GetCustomAttributes(typeof(T), true);
return attributes.Length > 0 ? attributes[0] as T : null;
}
public static MemberExpression GetMemberExpression<TModel, TProperty>(Expression<Func<TModel, TProperty>> expression)
{
MemberExpression memberExpression;
if (expression.Body is UnaryExpression)
{
var unaryExpression = (UnaryExpression)expression.Body;
memberExpression = (MemberExpression)unaryExpression.Operand;
}
else
{
memberExpression = (MemberExpression)expression.Body;
}
return memberExpression;
}
Run Code Online (Sandbox Code Playgroud)
当你想使用它时:
[ModelBinder(typeof(AliasModelBinder))]
public class FilterViewModel
{
[BindAlias("someText")]
public string FilterParameter { get; set; }
}
Run Code Online (Sandbox Code Playgroud)
在html中:
@* at least you dont write "someText" here again *@
@Html.Editor(Html.AliasNameFor(model => model.FilterParameter))
@Html.ValidationMessage(Html.AliasNameFor(model => model.FilterParameter))
Run Code Online (Sandbox Code Playgroud)
所以我在这里留下这个答案.这甚至不是一个答案(并且没有MVC 5的答案),但谁在谷歌搜索相同的问题可能会发现有用的这种体验.
这里是github回购:https://github.com/yusufuzun/so-view-model-bind-20869735
归档时间: |
|
查看次数: |
14384 次 |
最近记录: |