ASP.NET MVC Beta 1: DefaultModelBinder неправильно сохраняет параметр и состояние проверки между несвязанными запросами

Когда я использую привязку модели по умолчанию для привязки параметров формы к сложному объекту, который является параметром для действия, каркас запоминает значения, переданные первому запросу, что означает, что любой последующий запрос к этому действию получает те же данные, что и первый. Значения параметров и состояние проверки сохраняются между несвязанными веб-запросами.

Вот мой код контроллера (service представляет доступ к серверной части приложения):

    [AcceptVerbs(HttpVerbs.Get)]
    public ActionResult Create()
    {
        return View(RunTime.Default);
    }

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Create(RunTime newRunTime)
    {
        if (ModelState.IsValid)
        {
            service.CreateNewRun(newRunTime);
            TempData["Message"] = "New run created";
            return RedirectToAction("index");
        }
        return View(newRunTime);
    }

Мое представление ASPX (строго типизировано как ViewPage<RunTime>) содержит директивы, такие как:

<%= Html.TextBox("newRunTime.Time", ViewData.Model.Time) %>

Это использует DefaultModelBinder класс, который предназначен для автоматического связывания свойств моей модели.

Я попал на страницу, введите действительные данные (например, время = 1). Приложение правильно сохраняет новый объект со временем = 1. Затем я снова нажимаю на него, вводю другие действительные данные (например, время = 2). Однако сохраняемые данные являются оригинальными (например, время = 1). Это также влияет на проверку, поэтому, если мои исходные данные были недействительными, то все данные, которые я введу в будущем, будут считаться недействительными. Перезапуск IIS или перестройка моего кода сбрасывает постоянное состояние.

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

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Create([ModelBinder(typeof (RunTimeBinder))] RunTime newRunTime)
    {
        if (ModelState.IsValid)
        {
            service.CreateNewRun(newRunTime);
            TempData["Message"] = "New run created";
            return RedirectToAction("index");
        }
        return View(newRunTime);
    }


internal class RunTimeBinder : DefaultModelBinder
{
    public override ModelBinderResult BindModel(ModelBindingContext bindingContext)
    {
        // Without this line, failed validation state persists between requests
        bindingContext.ModelState.Clear();


        double time = 0;
        try
        {
            time = Convert.ToDouble(bindingContext.HttpContext.Request[bindingContext.ModelName + ".Time"]);
        }
        catch (FormatException)
        {
            bindingContext.ModelState.AddModelError(bindingContext.ModelName + ".Time", bindingContext.HttpContext.Request[bindingContext.ModelName + ".Time"] + "is not a valid number");
        }

        var model = new RunTime(time);
        return new ModelBinderResult(model);
    }
}

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

5 ответов

Решение

Оказывается, проблема была в том, что мои контроллеры использовались повторно между вызовами. Одна из деталей, которые я решил опустить в своем первоначальном посте, заключается в том, что я использую контейнер Castle.Windsor для создания своих контроллеров. Мне не удалось пометить мой контроллер временным образом жизни, поэтому я получал один и тот же экземпляр обратно при каждом запросе. Таким образом, контекст, используемый связывателем, использовался повторно и, конечно, он содержал устаревшие данные.

Я обнаружил проблему, тщательно анализируя разницу между кодом Эйлона и моим, исключая все другие возможности. Как говорится в документации Замка, это "ужасная ошибка"! Пусть это будет предупреждением для других!

Спасибо за ваш ответ, Эйлон. Извините, что занял ваше время.

Я не уверен, если это связано или нет, но ваш вызов <% = Html.TextBox ("newRunTime.Time", ViewData.Model.Time)%>может фактически выбрать неправильную перегрузку (так как Time является целым числом, это выберет object htmlAttributes перегрузка, а не string value,

Проверка визуализированного HTML позволит вам узнать, происходит ли это. изменение int на ViewData.Model.Time.ToString() вызовет правильную перегрузку.

Похоже, что ваша проблема - нечто иное, но я заметил это и был сожжен в прошлом.

Я пытался воспроизвести эту проблему, но я не вижу того же поведения. Я создал почти точно такой же контроллер и представления, которые у вас есть (с некоторыми допущениями), и каждый раз, когда я создавал новый "RunTime", я помещал его значение в TempData и отправлял его через Redirect. Затем на целевой странице я взял значение, и это всегда было значение, которое я вводил в этом запросе, а не устаревшее значение.

Вот мой контроллер:

открытый класс HomeController: Controller { public ActionResult Index() { ViewData["Title"] = "Home Page"; string message = "Welcome: " + TempData["Message"]; if (TempData.ContainsKey("value")) { int theValue = (int)TempData["value"]; message += " " + theValue.ToString(); } ViewData["Message"] = сообщение; возврат View (); }

[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Create() {
    return View(RunTime.Default);
}

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(RunTime newRunTime) {
    if (ModelState.IsValid) {
        //service.CreateNewRun(newRunTime);
        TempData["Message"] = "New run created";
        TempData["value"] = newRunTime.TheValue;
        return RedirectToAction("index");
    }
    return View(newRunTime);
}

}

А вот мой вид (Create.aspx):

<% using (Html.BeginForm()) { %>
<%= Html.TextBox("newRunTime.TheValue", ViewData.Model.TheValue) %>
<input type="submit" value="Save" />
<% } %>

Кроме того, я не был уверен, как выглядит тип "RunTime", поэтому я сделал это:

   public class RunTime {
        public static readonly RunTime Default = new RunTime(-1);

        public RunTime() {
        }

        public RunTime(int theValue) {
            TheValue = theValue;
        }

        public int TheValue {
            get;
            set;
        }
    }

Возможно ли, что ваша реализация RunTime включает в себя некоторые статические значения или что-то?

Спасибо,

Eilon

Себ, я не уверен, что ты имеешь в виду под примером. Я ничего не знаю о конфигурации Unity. Я объясню ситуацию с Castle.Windsor и, возможно, это поможет вам правильно настроить Unity.

По умолчанию Castle.Windsor возвращает один и тот же объект каждый раз, когда вы запрашиваете данный тип. Это синглтон образ жизни. В документации Castle.Windsor есть хорошее объяснение различных вариантов образа жизни.

В ASP.NET MVC каждый экземпляр класса контроллера связан с контекстом веб-запроса, который был создан для обслуживания. Поэтому, если ваш контейнер IoC каждый раз возвращает один и тот же экземпляр вашего класса контроллера, вы всегда будете привязывать контроллер к контексту первого веб-запроса, который использовал этот класс контроллера. В частности, ModelState и другие объекты, используемые DefaultModelBinder будет использоваться повторно, поэтому ваш связанный объект модели и сообщения проверки в ModelState будет несвежим.

Поэтому вам нужен ваш IoC для возврата нового экземпляра каждый раз, когда MVC запрашивает экземпляр вашего класса контроллера.

В Castle.Windsor это называется переходным образом жизни. Для его настройки у вас есть два варианта:

  1. Конфигурация XML: вы добавляете lifestlye="transient" к каждому элементу в вашем файле конфигурации, который представляет контроллер.
  2. Конфигурация в коде: вы можете указать контейнеру использовать переходный образ жизни во время регистрации контроллера. Это то, что помощник MvcContrib, о котором упоминал Бен, делает автоматически для вас - взгляните на метод RegisterControllers в исходном коде MvcContrib.

Я полагаю, что Unity предлагает похожую концепцию образа жизни в Castle.Windsor, поэтому вам нужно настроить Unity, чтобы использовать его эквивалент переходного образа жизни для ваших контроллеров. MvcContrib, кажется, имеет некоторую поддержку Unity - возможно, вы могли бы посмотреть там.

Надеюсь это поможет.

Столкнувшись с подобными проблемами при попытке использовать контейнер Windsor IoC в приложении ASP.NET MVC, мне пришлось пройти тот же путь обнаружения, чтобы заставить его работать. Вот некоторые детали, которые могут помочь кому-то еще.

Использование этого является начальной настройкой в ​​Global.asax:

  if (_container == null) 
  {
    _container = new WindsorContainer("config/castle.config");
    ControllerBuilder.Current.SetControllerFactory(new WindsorControllerFactory(Container)); 
  }

И используя WindsorControllerFactory, который при запросе экземпляра контроллера делает:

  return (IController)_container.Resolve(controllerType);

Хотя Виндзор правильно связывал все контроллеры, по какой-то причине параметры не передавались из формы в соответствующее действие контроллера. Вместо этого все они были нулевыми, хотя это вызывало правильное действие.

По умолчанию контейнер передает обратные синглтоны, что явно плохо для контроллеров и является причиной проблемы:

http://www.castleproject.org/monorail/documentation/trunk/integration/windsor.html

Однако в документации указывается, что образ жизни контроллеров может быть изменен на переходный, хотя на самом деле он не говорит вам, как это сделать, если вы используете файл конфигурации. Оказывается, это достаточно просто:

<component 
  id="home.controller" 
  type="DoYourStuff.Controllers.HomeController, DoYourStuff" 
  lifestyle="transient" />

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

Другие вопросы по тегам