Я был очарован этим вопросом, и я потратил немало времени на размышления о ваших целях. Вчера у меня был прорыв, и у меня есть код, который выполняет почти все ваши цели.
Вы сказали, что хотите, чтобы валидаторы сработали только тогда, когда была проверена Changed
. Этот код всегда запускает валидаторы, так как я не верю, что это хорошая практика, чтобы предотвратить запуск валидаторов. Вместо этого код проверяет, изменил ли пользователь значение, и автоматически проверяет Changed
, когда это происходит. Если пользователь снимает флажок «Изменено», старое значение помещается в поле Value
.
Код состоит из помощника HTML, ModelMetadataProvider, ModelBinder и просто небольшого JavaScript. Перед кодом приведена определенная модель, аналогичная модели Дарина, с добавлением одного дополнительного свойства:
public interface IChangeable
{
bool Changed { get; set; }
}
public class Changeable<T> : IChangeable
{
public bool Changed { get; set; }
public T Value { get; set; }
}
public class MyModel
{
[Range(1, 10), Display(Name = "Some Integer")]
public Changeable<int> SomeInt { get; set; }
[StringLength(32, MinimumLength = 6), Display(Name = "This String")]
public Changeable<string> TheString { get; set; }
}
Начиная с помощника HTML:
public static class HtmlHelperExtensions
{
public static MvcHtmlString ChangeableFor<TModel, TValue, TType>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, Changeable<TType> changeable)
{
var name = ExpressionHelper.GetExpressionText(expression);
if (String.IsNullOrEmpty(name))
throw new ArgumentNullException("name", "Name cannot be null");
var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
var type = metadata.ModelType;
var containerType = metadata.ContainerType;
var arg = Expression.Parameter(containerType, "x");
Expression expr = arg;
expr = Expression.Property(expr, name);
expr = Expression.Property(expr, "Value");
var funcExpr = Expression.Lambda(expr, arg) as Expression<Func<TModel, TType>>;
var valueModelMetadata = ModelMetadata.FromLambdaExpression(funcExpr, html.ViewData);
Expression exprChanged = arg;
exprChanged = Expression.Property(exprChanged, name);
exprChanged = Expression.Property(exprChanged, "Changed");
var funcExprChanged = Expression.Lambda(exprChanged, arg) as Expression<Func<TModel, bool>>;
var htmlSb = new StringBuilder("\n");
htmlSb.Append(LabelExtensions.Label(html, metadata.GetDisplayName()));
htmlSb.Append("<br />\n");
htmlSb.Append(InputExtensions.CheckBoxFor(html, funcExprChanged));
htmlSb.Append(" Changed<br />\n");
htmlSb.Append(InputExtensions.Hidden(html, name + ".OldValue", valueModelMetadata.Model) + "\n");
htmlSb.Append(EditorExtensions.EditorFor(html, funcExpr, new KeyValuePair<string, object>("parentMetadata", metadata)));
htmlSb.Append(ValidationExtensions.ValidationMessageFor(html, funcExpr));
htmlSb.Append("<br />\n");
return new MvcHtmlString(htmlSb.ToString());
}
}
Это передает родительские метаданные в ViewData
(что позволит нам позже получить валидаторы класса). Он также создает лямбда-выражения, поэтому мы можем использовать CheckBoxFor()
и EditorFor()
. Вид с использованием нашей модели и этого помощника выглядит следующим образом:
@model MyModel
@{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}
@using (Html.BeginForm())
{
<script type="text/javascript">
$(document).ready(function () {
$("input[id$='Value']").live("keyup blur", function () {
var prefix = this.id.split("_")[0];
var oldValue = $("#" + prefix + "_OldValue").val();
var changed = oldValue != $(this).val()
$("#" + prefix + "_Changed").attr("checked", changed);
if (changed) {
// validate
$(this.form).validate().element($("#" + prefix + "_Value")[0]);
}
});
$("input[id$='Changed']").live("click", function () {
if (!this.checked) {
// replace value with old value
var prefix = this.id.split("_")[0];
var oldValue = $("#" + prefix + "_OldValue").val();
$("#" + prefix + "_Value").val(oldValue);
// validate
$(this.form).validate().element($("#" + prefix + "_Value")[0]);
}
});
});
</script>
@Html.ChangeableFor(x => x.SomeInt, Model.SomeInt)
@Html.ChangeableFor(x => x.TheString, Model.TheString)
<input type="submit" value="Submit" />
}
Обратите внимание на JavaScript для работы с изменениями в текстовом поле «Значение» и щелчком на флажке «Изменено». Также обратите внимание на необходимость дважды передавать свойство Changeable<T>
помощнику ChangeableFor()
.
Далее пользовательский ModelValidatorProvider:
public class MyDataAnnotationsModelValidatorProvider : DataAnnotationsModelValidatorProvider
{
private bool _provideParentValidators = false;
protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
{
if (metadata.ContainerType != null && metadata.ContainerType.Name.IndexOf("Changeable") > -1 && metadata.PropertyName == "Value")
{
var viewContext = context as ViewContext;
if (viewContext != null)
{
var viewData = viewContext.ViewData;
var index = viewData.Keys.ToList().IndexOf("Value");
var parentMetadata = viewData.Values.ToList()[index] as ModelMetadata;
_provideParentValidators = true;
var vals = base.GetValidators(parentMetadata, context);
_provideParentValidators = false;
return vals;
}
else
{
var viewData = context.Controller.ViewData;
var keyName = viewData.ModelState.Keys.ToList().Last().Split(new string[] { "." }, StringSplitOptions.None).First();
var index = viewData.Keys.ToList().IndexOf(keyName);
var parentMetadata = viewData.Values.ToList()[index] as ModelMetadata;
parentMetadata.Model = metadata.Model;
_provideParentValidators = true;
var vals = base.GetValidators(parentMetadata, context);
_provideParentValidators = false;
return vals;
}
}
else if (metadata.ModelType.Name.IndexOf("Changeable") > -1 && !_provideParentValidators)
{
// DO NOT provide parent's validators, unless it is at the request of the child Value property
return new List<ModelValidator>();
}
return base.GetValidators(metadata, context, attributes).ToList();
}
}
Обратите внимание, что существуют разные способы проверки родительских метаданных в зависимости от того, заполняем ли мы представление или привязываем модель к POST. Также обратите внимание, что нам нужно запретить родителю получать валидаторы.
Наконец, ModelBinder:
public class ChangeableModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (controllerContext.Controller.ViewData.Keys.ToList().IndexOf(bindingContext.ModelName) < 0)
controllerContext.Controller.ViewData.Add(bindingContext.ModelName, bindingContext.ModelMetadata);
return base.BindModel(controllerContext, bindingContext);
}
}
Это берет родительские метаданные и хранит их для последующего доступа в пользовательском ModelValidatorProvider.
Завершите следующее в Application_Start
в Global.asax.cs:
ModelValidatorProviders.Providers.Clear();
ModelValidatorProviders.Providers.Add(new MvcApplication5.Extensions.MyDataAnnotationsModelValidatorProvider());
MyDataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;
ModelBinders.Binders.Add(typeof(MvcApplication5.Controllers.Changeable<int>), new ChangeableModelBinder());
ModelBinders.Binders.Add(typeof(MvcApplication5.Controllers.Changeable<string>), new ChangeableModelBinder());
// you must add a ModelBinders.Binders.Add() declaration for each type T you
// will use in your Changeable<T>
Viola!