Обновление ModelState с помощью объекта модели - PullRequest
12 голосов
/ 05 апреля 2009

Проблема: Как обновить ModelState в сценарии публикации + проверки.

У меня есть простая форма:

<%= Html.ValidationSummary() %>
<% using(Html.BeginForm())%>
<%{ %>
    <%=Html.TextBox("m.Value") %>
    <input type="submit" />
<%} %>

Когда пользователь отправляет сообщение, я хочу проверить ввод и в некоторых случаях я хочу исправить ошибку для пользователя, сообщив ему, что он сделал ошибку, которая уже исправлена:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Index(M m)
{
    if (m.Value != "a")
    {
        ModelState.AddModelError("m.Value", "should be \"a\"");
        m.Value = "a";
        return View(m);
    }
    return View("About");            
}

Что ж, проблема в том, что MVC просто проигнорирует модель, переданную представлению, и выполнит повторную визуализацию независимо от того, что набрал пользователь, а не мое значение ("a"). Это происходит потому, что средство визуализации TextBox проверяет, существует ли ModelState, и если оно не равно нулю, используется значение ModelState. Это значение, конечно, один пользователь набрал перед публикацией.

Поскольку я не могу изменить поведение рендерера TextBox, единственное решение, которое я нашел, - это обновить ModelState самостоятельно. Быстрый и грязный способ состоит в том, чтобы (ab) использовать DefaultModelBinder и переопределить метод, который присваивает значения из форм модели, просто изменяя направление назначения;). Используя DefaultModelBinder мне не нужно анализировать идентификаторы. Следующий код (основанный на оригинальной реализации DefaultModelBinder) является моим решением для этого:

/// <summary>
    /// Updates ModelState using values from <paramref name="order"/>
    /// </summary>
    /// <param name="order">Source</param>
    /// <param name="prefix">Prefix used by Binder. Argument name in Action (if not explicitly specified).</param>
    protected void UpdateModelState(object model, string prefix)
    {
        new ReversedBinder().BindModel(this.ControllerContext,
            new ModelBindingContext()
            {
                Model = model,
                ModelName = prefix,
                ModelState = ModelState,
                ModelType = model.GetType(),
                ValueProvider = ValueProvider
            });
    }

    private class ReversedBinder : DefaultModelBinder
    {
        protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
        {
            string prefix = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
            object val = typeof(Controller)
                .Assembly.GetType("System.Web.Mvc.DictionaryHelpers")
                .GetMethod("DoesAnyKeyHavePrefix")
                .MakeGenericMethod(typeof(ValueProviderResult))
                .Invoke(null, new object[] { bindingContext.ValueProvider, prefix });
            bool res = (bool)val;
            if (res)
            {

                IModelBinder binder = new ReversedBinder();//this.Binders.GetBinder(propertyDescriptor.PropertyType);
                object obj2 = propertyDescriptor.GetValue(bindingContext.Model);

                ModelBindingContext context2 = new ModelBindingContext();
                context2.Model = obj2;
                context2.ModelName = prefix;
                context2.ModelState = bindingContext.ModelState;
                context2.ModelType = propertyDescriptor.PropertyType;
                context2.ValueProvider = bindingContext.ValueProvider;
                ModelBindingContext context = context2;
                object obj3 = binder.BindModel(controllerContext, context);

                if (bindingContext.ModelState.Keys.Contains<string>(prefix))
                {
                    var prefixKey = bindingContext.ModelState.Keys.First<string>(x => x == prefix);
                    bindingContext.ModelState[prefixKey].Value
                                    = new ValueProviderResult(obj2, obj2.ToString(),
                                                                bindingContext.ModelState[prefixKey].Value.Culture);
                }
            }
        }
    }

Таким образом, остается вопрос: я делаю что-то необычное или я что-то упускаю? Если первое, то как я мог бы реализовать такую ​​функциональность лучше (используя существующую инфраструктуру MVC)?

Ответы [ 3 ]

23 голосов
/ 05 января 2011

Я знаю, что этот пост довольно старый, но это проблема, с которой я сталкивался раньше, и я просто подумал о простом решении, которое мне нравится - просто очистите ModelState после того, как вы получите опубликованные значения.

UpdateModel(viewModel);
ModelState.Clear();

viewModel.SomeProperty = "a new value";
return View(viewModel);

и представление должно использовать (возможно измененный) объект модели представления, а не ModelState.

Может быть, это действительно очевидно. Оглядываясь назад, кажется, что так!

4 голосов
/ 06 апреля 2009

Вы можете принять коллекцию форм в качестве параметра вместо объекта модели в контроллере, например: public ActionResult Index(FormCollection Form).

Поэтому связыватель модели по умолчанию не будет обновлять состояние модели, и вы получите желаемое поведение.

Редактировать : Или вы можете просто обновить ModelStateDictionary, чтобы отразить ваши изменения в модели.


[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Index(M m)
{
    if (m.Value != "a")
    {
        ModelState["m.Value"].Value = new ValueProviderResult("a", m.Name, 
                    CultureInfo.CurrentCulture);
        ModelState.AddModelError("m.Value", "should be \"a\"");
        m.Value = "a";
        return View(m);
    }
    return View("About");            
}

Примечание: я не уверен, что это лучший способ. Но, похоже, это работает, и это должно быть поведение, которое вы хотите.

0 голосов
/ 27 марта 2014

я делаю что-то необычное или я что-то упускаю?

Я думаю, это довольно редко. Я думаю, что MVC предполагает, что ошибки валидации являются делом «да / нет», и в этом случае вы используете ошибку валидации в качестве средства предоставления общих отзывов пользователей.

Я думаю, что MVC также кажется более счастливым, когда POST-файлы либо терпят неудачу из-за ошибок валидации, либо выполняют действие и перенаправляют или отображают что-то совершенно другое. За исключением ошибок проверки модели, довольно редко повторный рендеринг одного и того же ввода.

Я использую MVC уже около года и просто столкнулся с этим в другом контексте, где после POST я хотел отобразить свежую форму в качестве ответа.

[HttpPost]
public ActionResult Upload(DocumentView data) {
   if(!ModelState.IsValid) return View(data);
   ProcessUpload(data);
   return View(new DocumentView());
}

MVC рендерит ModelState из data, а не моего нового объекта. Очень удивительно.

Если первое, то как я мог бы реализовать такую ​​функциональность лучше

  1. внедрить автоматические исправления в javascript (может быть невозможно)
  2. вести список сделанных автоматических исправлений, если объект после всех этих объектов действителен, затем передать его в представление «О программе» и отобразить в виде сообщения типа «M сохранено» со следующими исправлениями: ... ».
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...