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);
    }
}

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


person Alex Scordellis    schedule 26.10.2008    source источник


Ответы (5)


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

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

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

person Alex Scordellis    schedule 27.10.2008
comment
Эта ТОЧНАЯ вещь случалась со мной раньше. Мне потребовалось много времени, чтобы понять это. В будущем позвольте MvcContrib регистрировать ваши контроллеры, используя их методы расширения WindsorContainer. - person Ben Scheirman; 28.10.2008
comment
Хороший совет - спасибо Бен. Вы должны были поместить это как ответ, а не комментарий, чтобы получить голос и зеленую галочку! - person Alex Scordellis; 28.10.2008
comment
Спасибо, Алекс, большую часть дня я бился над этой проблемой. - person Sean Campbell; 27.01.2009
comment
В моем случае я создал пользовательский модуль связывания модели (расширенный из DefaultModelBinder) и сохранил переменную экземпляра, не понимая, что модуль связывания модели можно повторно использовать в последующих запросах. - person Nicholas Piasecki; 22.04.2009

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

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

открытый класс HomeController: Controller { public ActionResult Index() { ViewData["Title"] = "Домашняя страница"; string message = "Добро пожаловать: " + TempData["Message"]; if (TempData.ContainsKey("value")) { int theValue = (int)TempData["value"]; сообщение += " " + theValue.ToString(); } ViewData["Сообщение"] = сообщение; вернуть вид(); }

[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 включает какие-то статические значения или что-то в этом роде?

Спасибо,

Эйлон

person Eilon    schedule 27.10.2008

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

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

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

person Ben Scheirman    schedule 27.10.2008
comment
Спасибо за предложение, Бен. На этот раз проблема не в этом, но похоже на то, о чем мне нужно знать в будущем, так что спасибо, что обратили на это мое внимание. - person Alex Scordellis; 27.10.2008

Себ, я не уверен, что ты имеешь в виду под примером. Я ничего не знаю о конфигурации 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 — возможно, вы могли бы посмотреть там.

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

person Alex Scordellis    schedule 02.11.2008

Столкнувшись с подобными проблемами при попытке использовать контейнер 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);

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

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

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

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

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

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

person Jason    schedule 03.11.2008