Проверка и редактирование «Изменяемого» / необязательного типа в Asp.net MVC 3

#c# #asp.net #asp.net-mvc #asp.net-mvc-3

#c# #asp.net #asp.net-mvc #asp.net-mvc-3

Вопрос:

для проекта мне нужен способ указать, изменилось ли свойство.

Итак, в модели я хотел бы следующее:

 public class Changeable<T>
{
   public bool Changed { get; set; }
   public T Value { get; set; }
}

public class MyModel
{
    [Range(1, 10)]
    public Changeable<int> SomeInt { get; set; }
}
  

Вид:

 @Html.EditorFor(model => model.SomeInt)
  

И затем я бы сгенерировал редактор с текстовым полем (int) и флажком (изменен).
Атрибуты проверки ( Range и т.д.) Должны вызываться, когда флажок установлен, но не когда он снят.

Я попытался выполнить вышеуказанное с помощью editortemplate для Changeable (а затем идет проверка, привязка модели и т.д.), Но я уже сбит с толку editortemplate, потому что он не может быть универсальным.

Возможно ли решение, которое я хочу, или есть другое разумное и элегантное решение?

Прямо сейчас я разрабатываю решение со свойством string[] ChangedProperties и большим количеством Javascript для обработки проверки и т.д. Но это довольно уродливо и далеко от завершения.

Спасибо…

Ответ №1:

Вы можете попробовать использовать динамические типы с настраиваемым атрибутом проверки диапазона:

 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
{
    [MyRange(1, 10)]
    public Changeable<int> SomeInt { get; set; }
}

public class MyRangeAttribute : RangeAttribute
{
    public MyRangeAttribute(double minimum, double maximum): base(minimum, maximum)
    { }

    public MyRangeAttribute(int minimum, int maximum)
        : base(minimum, maximum)
    { }

    public override bool IsValid(object value)
    {
        var changeable = value as IChangeable;
        if (changeable == null || !changeable.Changed)
        {
            return true;
        }
        dynamic dynValue = value;

        return base.IsValid((object)dynValue.Value);
    }
}
  

затем контроллер:

 public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View(new MyModel
        {
            SomeInt = new Changeable<int>
            {
                Changed = true,
                Value = 5
            }
        });
    }

    [HttpPost]
    public ActionResult Index(MyModel model)
    {
        return View(model);
    }
}
  

затем представление ( ~/Views/Home/Index.cshtml ):

 @model MyModel

@using (Html.BeginForm())
{
    @Html.EditorFor(x => x.SomeInt)
    <button type="submit">OK</button>
}
  

и соответствующего шаблона редактора (обратите внимание на имя файла шаблона редактора)

 ~/Views/Shared/EditorTemplates/Changeable`1.cshtml
  

 @model dynamic
@Html.CheckBox("Changed", (bool)Model.Changed)
@Html.Editor("Value")
@Html.ValidationMessage("")
  

Комментарии:

1. Ах 🙂 Я не подумал о @model dynamic . Тем не менее, я попробовал, @model Changeable<dynamic> для чего требуется object . Спасибо! У меня есть много разных атрибутов проверки, возможно ли создать средство проверки, для Changeable которого распространяется значение?

2. @lasseespeholt, я не понимаю, что вы имеете в виду под validator for Changeable which propagates the value . Какое распространение? Кстати, если вы хотите сделать что-то другое, кроме тривиальной проверки , я бы рекомендовал вам использовать FluentValidation. NET вместо аннотаций данных. Это отличная библиотека, которую я использую с большим успехом. Декларативная проверка с аннотациями данных просто не может справиться со сложными сценариями проверки.

3. Перефразировано: возможно ли сохранить аннотации данных из библиотеки, такие как Range вместо MyRange . Может быть, выполнив какую-нибудь пользовательскую клиентскую проверку и привязку к модели для Changeable ? Я предполагаю, что я теряю клиентскую проверку при использовании MyRange .

4. @lasseespeholt, конечно, из-за этого вы потеряете проверку на стороне клиента. Здесь у вас есть какое-то пользовательское правило проверки (применяйте проверку, только если установлено значение какого-либо флажка). Для этого вам также придется написать пользовательскую логику проверки на клиенте. Клиент не может знать об этой логике. Итак, чтобы ответить на ваш вопрос, нет, вы не можете полагаться на встроенные атрибуты, вам придется написать свои собственные пользовательские атрибуты и не только это, но если вы хотите клиентскую проверку, вам также придется делать это вручную.

Ответ №2:

Я был очарован этим вопросом и потратил немало времени на размышления о ваших целях. Вчера у меня был прорыв, и у меня есть некоторый код, который выполняет почти все ваши задачи.

Вы сказали, что хотите, чтобы средства проверки запускались только тогда, когда 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 для обработки изменений в текстовом поле Value и щелчки по измененному флажку. Также обратите внимание на необходимость дважды передавать 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 amp;amp; metadata.ContainerType.Name.IndexOf("Changeable") > -1 amp;amp; 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 amp;amp; !_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>
  

Виола!

Ответ №3:

Я не уверен на 100%, но я думаю, что вы хотите:

 private int _changeable;

public Changeable<int> SomeInt { 
get 
    { return _changeable } 
set 
    { _changeable = value;
      Changed = true;
    }
}
  

Комментарии:

1. Нет, я хочу Asp.net Редактор MVC и привязка для Changeable свойства. Прошу прощения, если это было непонятно. Я попытался сделать это более понятным.