ASP.NET MVC 的自定义模型属性别名绑定

最近在研究 ASP.NET MVC 模型绑定,发现 DefaultModelBinder 有一个弊端,就是无法实现对浏览器请求参数的自定义,最初的想法是想为实体模型的属性设置特性(Attribute),然后通过取得设置的特性值对属性进行赋值,研究了好久 MVC 源码之后发现可以通过重写 DefaultModelBinder 的 BindProperty 方法可以达到预期的目的。

ASP.NET MVC 中有一个自定义模型绑定特性 CustomModelBinderAttribute,打算通过重写 CustomModelBinderAttribute 来对实体属性进行出来,实现如下:

1
2
3
[AttributeUsageAttribute(AttributeTargets.Class|AttributeTargets.Struct|AttributeTargets.Enum|AttributeTargets.Interface|AttributeTargets.Parameter, AllowMultiple = false,
    Inherited = false)]
public abstract class CustomModelBinderAttribute : Attribute

但是由于 CustomModelBinderAttribute 不支持对属性设置特性,所以只好继承 Attribute 类重新写一个特性,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/// <summary>
/// 表示一个调用自定义模型联编程序的特性。
/// </summary>
[AttributeUsage(ValidTargets, AllowMultiple = false, Inherited = false)]
public class PropertyModelBinderAttribute : Attribute
{
    /// <summary>
    /// 指定此属性可以应用特性的应用程序元素。
    /// </summary>
    internal const AttributeTargets ValidTargets = AttributeTargets.Field | AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Parameter;
    /// <summary>
    /// 声明属性名称。
    /// </summary>
    private string _propertyName = string.Empty;
 
    /// <summary>
    /// 获取或设置属性别名。
    /// </summary>
    public string PropertyName
    {
        get { return _propertyName; }
    }
 
    /// <summary>
    /// 使用指定的属性别名。
    /// </summary>
    /// <param name="propertyName">指定的属性别名。</param>
    public PropertyModelBinderAttribute(string propertyName)
    {
        _propertyName = propertyName;
    }
 
    /// <summary>
    /// 检索关联的模型联编程序。。
    /// </summary>
    /// <returns>对实现 System.Web.Mvc.IModelBinder 接口的对象的引用。</returns>
    public IModelBinder GetBinder()
    {
        return new PropertyModelBinder();
    }

这样就可以在实体模型的属性上设置别名了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// <summary>
/// 表示一个城市筛选实体对象模型。
/// </summary>
[ModelBinder(typeof(PropertyModelBinder))]
public class CityFilteringModel : BaseEntityModel
{
 
    /// <summary>
    /// 获取或设置城市英文名称。
    /// </summary>
    public string CityEnglishName { get; set; }
    /// <summary>
    /// 获取或设置城市编号。
    /// </summary>
    [PropertyModelBinder("cid")]
    public int CityId { get; set; }
    /// <summary>
    /// 获取或设置城市名称。
    /// </summary>
    [PropertyModelBinder("cname")]
    public string CityName { get; set; }
}

最后听过重写 DefaultModelBinder 的 BindProperty 和 SetProperty 方法就可以对模型绑定的属性实现自定义了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
/// <summary>
/// 将浏览器请求映射到数据对象。
/// </summary>
public class PropertyModelBinder : DefaultModelBinder
{
 
    /// <summary>
    /// 初始化 <see cref="PropertyModelBinder"/> 类的新实例。
    /// </summary>
    public PropertyModelBinder()
    {
    }
 
    /// <summary>
    /// 使用指定的控制器上下文和绑定上下文来绑定模型。
    /// </summary>
    /// <param name="controllerContext">运行控制器的上下文。</param>
    /// <param name="bindingContext">绑定模型的上下文。</param>
    /// <returns>已绑定的对象。</returns>
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var model = base.BindModel(controllerContext, bindingContext);
        if (model is BaseEntiryModel) ((BaseEntiryModel)model).BindModel(controllerContext, bindingContext);
        return model;
    }
 
    /// <summary>
    ///  使用指定的控制器上下文、绑定上下文、属性描述符和属性联编程序来返回属性值。
    /// </summary>
    /// <param name="controllerContext">运行控制器的上下文。</param>
    /// <param name="bindingContext">绑定模型的上下文。</param>
    /// <param name="propertyDescriptor">要访问的属性的描述符。</param>
    /// <param name="propertyBinder">一个对象,提供用于绑定属性的方式。</param>
    /// <returns>一个对象,表示属性值。</returns>
    protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
    {
        var value = base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
 
        return value;
    }
 
    /// <summary>
    /// 使用指定的控制器上下文、绑定上下文和指定的属性描述符来绑定指定的属性。
    /// </summary>
    /// <param name="controllerContext">运行控制器的上下文。</param>
    /// <param name="bindingContext">绑定模型的上下文。</param>
    /// <param name="propertyDescriptor">描述要绑定的属性。</param>
    protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
    {
        string fullPropertyKey = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
        object propertyValue = null;
 
        if (propertyDescriptor.Attributes[typeof(PropertyModelBinderAttribute)] != null)
        {
            var attribute = (PropertyModelBinderAttribute)propertyDescriptor.Attributes[typeof(PropertyModelBinderAttribute)];
            string propertyName = attribute.PropertyName;
            var valueResult = bindingContext.ValueProvider.GetValue(propertyName);
 
            if (valueResult != null)
                propertyValue = valueResult.AttemptedValue;
        }
        else
        {
            if (!bindingContext.ValueProvider.ContainsPrefix(fullPropertyKey))
            {
                return;
            }
        }
 
        // call into the property's model binder
        IModelBinder propertyBinder = Binders.GetBinder(propertyDescriptor.PropertyType);
        object originalPropertyValue = propertyDescriptor.GetValue(bindingContext.Model);
        ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name];
        propertyMetadata.Model = originalPropertyValue;
        ModelBindingContext innerBindingContext = new ModelBindingContext()
        {
            ModelMetadata = propertyMetadata,
            ModelName = fullPropertyKey,
            ModelState = bindingContext.ModelState,
            ValueProvider = bindingContext.ValueProvider
        };
        object newPropertyValue = GetPropertyValue(controllerContext, innerBindingContext, propertyDescriptor, propertyBinder);
        if (newPropertyValue == null)
        {
            newPropertyValue = propertyValue;
        }
 
        propertyMetadata.Model = newPropertyValue;
        // validation
        ModelState modelState = bindingContext.ModelState[fullPropertyKey];
 
        if (modelState == null || modelState.Errors.Count == 0)
        {
            if (OnPropertyValidating(controllerContext, bindingContext, propertyDescriptor, newPropertyValue))
            {
                SetProperty(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
                OnPropertyValidated(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
            }
        }
        else
        {
            SetProperty(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
 
            // Convert FormatExceptions (type conversion failures) into InvalidValue messages
            foreach (ModelError error in modelState.Errors.Where(err => String.IsNullOrEmpty(err.ErrorMessage) && err.Exception != null).ToList())
            {
                for (Exception exception = error.Exception; exception != null; exception = exception.InnerException)
                {
                    // We only consider "known" type of exception and do not make too aggressive changes here
                    if (exception is FormatException || exception is OverflowException)
                    {
                        string displayName = propertyMetadata.GetDisplayName();
                        string errorMessageTemplate = GetValueInvalidResource(controllerContext);
                        string errorMessage = String.Format(CultureInfo.CurrentCulture, errorMessageTemplate, modelState.Value.AttemptedValue, displayName);
                        modelState.Errors.Remove(error);
                        modelState.Errors.Add(errorMessage);
                        break;
                    }
                }
            }
        }
        //base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
    }
 
    /// <summary>
    /// 使用指定的控制器上下文、绑定上下文和属性值来设置指定的属性。
    /// </summary>
    /// <param name="controllerContext">运行控制器的上下文。</param>
    /// <param name="bindingContext">绑定模型的上下文。</param>
    /// <param name="propertyDescriptor">描述要绑定的属性。</param>
    /// <param name="value">为属性设置的值。</param>
    protected override void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
    {
        ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name];
        propertyMetadata.Model = value;
        string modelStateKey = CreateSubPropertyName(bindingContext.ModelName, propertyMetadata.PropertyName);
 
        if (value == null && bindingContext.ModelState.IsValidField(modelStateKey))
        {
            ModelValidator requiredValidator = ModelValidatorProviders.Providers.GetValidators(propertyMetadata, controllerContext).Where(v => v.IsRequired).FirstOrDefault();
            if (requiredValidator != null)
            {
                foreach (ModelValidationResult validationResult in requiredValidator.Validate(bindingContext.Model))
                {
                    bindingContext.ModelState.AddModelError(modelStateKey, validationResult.Message);
                }
            }
        }
 
        bool isNullValueOnNonNullableType = value == null && !TypeAllowsNullValue(propertyDescriptor.PropertyType);
 
        if (!propertyDescriptor.IsReadOnly && !isNullValueOnNonNullableType)
        {
            try
            {
                var typeValue = Convert.ChangeType(value, propertyDescriptor.PropertyType, CultureInfo.InvariantCulture);
                propertyDescriptor.SetValue(bindingContext.Model, typeValue);
            }
            catch (Exception ex)
            {
                if (bindingContext.ModelState.IsValidField(modelStateKey))
                {
                    bindingContext.ModelState.AddModelError(modelStateKey, ex);
                }
            }
        }
 
        if (isNullValueOnNonNullableType && bindingContext.ModelState.IsValidField(modelStateKey))
        {
            bindingContext.ModelState.AddModelError(modelStateKey, GetValueRequiredResource(controllerContext));
        }
    }
 
 
 
    /// <summary>
    /// 使用指定的控制器上下文和绑定上下文来返回模型的属性。
    /// </summary>
    /// <param name="controllerContext">运行控制器的上下文。</param>
    /// <param name="bindingContext">绑定模型的上下文。</param>
    /// <returns>属性描述符的集合。</returns>
    protected override PropertyDescriptorCollection GetModelProperties(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        bindingContext.PropertyFilter = new Predicate<string>(pred);
        var values = base.GetModelProperties(controllerContext, bindingContext);
        return values;
    }
 
    /// <summary>
    /// 获取属性筛选器的判定对象。
    /// </summary>
    /// <param name="target">属性筛选器的属性。</param>
    /// <returns>一个布尔值。</returns>
    protected bool pred(string target)
    {
        return true;
    }
 
    #region Private ...
 
    /// <summary>
    /// 类型允许空值。
    /// </summary>
    /// <param name="type">指定的类型。</param>
    /// <returns>若类型值为空,则返回 true,否则返回 false。</returns>
    private static bool TypeAllowsNullValue(Type type)
    {
        return (!type.IsValueType || IsNullableValueType(type));
    }
 
    /// <summary>
    /// 是可为空值类型。
    /// </summary>
    /// <param name="type">指定的类型。</param>
    /// <returns>若类型值为空,则返回 true,否则返回 false。</returns>
    private static bool IsNullableValueType(Type type)
    {
        return Nullable.GetUnderlyingType(type) != null;
    }
 
    /// <summary>
    /// 获取价值无效的资源。
    /// </summary>
    /// <param name="controllerContext"></param>
    /// <returns></returns>
    private static string GetValueInvalidResource(ControllerContext controllerContext)
    {
        return GetUserResourceString(controllerContext, "PropertyValueInvalid") ?? "The value '{0}' is not valid for {1}.";
    }
 
    /// <summary>
    /// 获取价值所需的资源。
    /// </summary>
    /// <param name="controllerContext"></param>
    /// <returns></returns>
    private static string GetValueRequiredResource(ControllerContext controllerContext)
    {
        return GetUserResourceString(controllerContext, "PropertyValueRequired") ?? "A value is required.";
    }
 
    private static string GetUserResourceString(ControllerContext controllerContext, string resourceName)
    {
        string result = null;
 
        if (!String.IsNullOrEmpty(ResourceClassKey) && (controllerContext != null) && (controllerContext.HttpContext != null))
        {
            result = controllerContext.HttpContext.GetGlobalResourceObject(ResourceClassKey, resourceName, CultureInfo.CurrentUICulture) as string;
        }
 
        return result;
    }
 
    #endregion
 
}

需要注意的是要在实体模型的类上设置 [ModelBinder(typeof(PropertyModelBinder))] 并且在 Global 中注册。

1
2
ModelBinders.Binders.Clear();
ModelBinders.Binders.Add(typeof(PropertyModelBinder), new PropertyModelBinder());

  

 

posted @   Charles Zhang  阅读(4797)  评论(7编辑  收藏  举报
编辑推荐:
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
点击右上角即可分享
微信分享提示