ASP.NET MVC - как предотвратить ошибки ModelState в RedirectToAction?


92

У меня есть два следующих метода действий (упрощенные для вопроса):

[HttpGet]
public ActionResult Create(string uniqueUri)
{
   // get some stuff based on uniqueuri, set in ViewData.  
   return View();
}

[HttpPost]
public ActionResult Create(Review review)
{
   // validate review
   if (validatedOk)
   {
      return RedirectToAction("Details", new { postId = review.PostId});
   }  
   else
   {
      ModelState.AddModelError("ReviewErrors", "some error occured");
      return RedirectToAction("Create", new { uniqueUri = Request.RequestContext.RouteData.Values["uniqueUri"]});
   }   
}

Итак, если проверка проходит, я перенаправляюсь на другую страницу (подтверждение).

Если возникает ошибка, мне нужно отобразить ту же страницу с ошибкой.

Если я это сделаю return View(), отобразится ошибка, но если я сделаю return RedirectToAction(как указано выше), он потеряет ошибки модели.

Я не удивлен этой проблемой, просто интересно, как вы с этим справляетесь?

Конечно, я мог бы просто вернуть то же представление вместо перенаправления, но у меня есть логика в методе «Create», который заполняет данные представления, которые мне пришлось бы продублировать.

Какие-либо предложения?


10
Я решаю эту проблему, не используя шаблон Post-Redirect-Get для ошибок проверки. Я просто использую View (). Совершенно верно сделать это вместо того, чтобы прыгать через кучу обручей и перенаправлять беспорядок в истории вашего браузера.
Джимми Богард,

2
И в дополнение к тому, что сказал @JimmyBogard, извлеките логику в Createметоде, который заполняет ViewData, и вызовите его в Createметоде GET, а также в ветке неудачной проверки в Createметоде POST.
Russ Cam

1
Согласен, избегание проблемы - это один из способов ее решения. У меня есть некоторая логика для заполнения материала в моем Createпредставлении, я просто помещаю ее в какой-то метод, populateStuffкоторый я вызываю как в, так GETи в ошибке POST.
Francois Joly

12
@JimmyBogard Я не согласен, если вы отправляете сообщение в действие, а затем возвращаете представление, с которым столкнулись с проблемой, когда, если пользователь нажимает кнопку обновления, он получает предупреждение о желании снова инициировать этот пост.
The Muffin Man

Ответы:


50

В Reviewвашем HttpGetдействии должен быть такой же экземпляр . Для этого вы должны сохранить объект Review reviewво временной переменной вашего HttpPostдействия, а затем восстановить его при HttpGetдействии.

[HttpGet]
public ActionResult Create(string uniqueUri)
{
   //Restore
   Review review = TempData["Review"] as Review;            

   // get some stuff based on uniqueuri, set in ViewData.  
   return View(review);
}
[HttpPost]
public ActionResult Create(Review review)
{
   //Save your object
   TempData["Review"] = review;

   // validate review
   if (validatedOk)
   {
      return RedirectToAction("Details", new { postId = review.PostId});
   }  
   else
   {
      ModelState.AddModelError("ReviewErrors", "some error occured");
      return RedirectToAction("Create", new { uniqueUri = Request.RequestContext.RouteData.Values["uniqueUri"]});
   }   
}

Если вы хотите, чтобы это работало, даже если браузер обновляется после первого выполнения HttpGetдействия, вы можете сделать это:

  Review review = TempData["Review"] as Review;  
  TempData["Review"] = review;

В противном случае объект кнопки обновления reviewбудет пустым, потому что в нем не будет никаких данных TempData["Review"].


2
Превосходно. И большой +1 за упоминание о проблеме с обновлением. Это наиболее полный ответ, поэтому я приму его, большое спасибо. :)
RPM1984

8
Это не совсем ответ на вопрос в заголовке. ModelState не сохраняется, и это имеет такие последствия, как ввод HtmlHelpers, не сохраняющий запись пользователя. Это почти обходной путь.
Джон Фаррелл

В итоге я сделал то, что @Wim предложил в его ответе.
RPM1984

17
@jfar, я согласен, этот ответ не работает и не сохраняет ModelState. Однако, если вы измените его так, чтобы он делал что-то подобное, TempData["ModelState"] = ModelState; и восстанавливал с помощью ModelState.Merge((ModelStateDictionary)TempData["ModelState"]);, тогда он будет работать
asgeo1

1
Не могли бы вы только return Create(uniqueUri)тогда, когда проверка POST не удалась? Поскольку значения ModelState имеют приоритет над ViewModel, переданным в представление, опубликованные данные все равно должны оставаться.
ajbeaven

84

Мне пришлось сегодня самому решать эту проблему, и я столкнулся с этим вопросом.

Некоторые ответы полезны (с использованием TempData), но на самом деле не отвечают на поставленный вопрос.

Лучший совет, который я нашел, был в этом сообщении в блоге:

http://www.jefclaes.be/2012/06/persisting-model-state-when-using-prg.html

По сути, используйте TempData для сохранения и восстановления объекта ModelState. Однако будет намного чище, если вы абстрагируете это в атрибуты.

Например

public class SetTempDataModelStateAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        base.OnActionExecuted(filterContext);         
        filterContext.Controller.TempData["ModelState"] = 
           filterContext.Controller.ViewData.ModelState;
    }
}

public class RestoreModelStateFromTempDataAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        base.OnActionExecuting(filterContext);
        if (filterContext.Controller.TempData.ContainsKey("ModelState"))
        {
            filterContext.Controller.ViewData.ModelState.Merge(
                (ModelStateDictionary)filterContext.Controller.TempData["ModelState"]);
        }
    }
}

Затем, согласно вашему примеру, вы можете сохранить / восстановить ModelState следующим образом:

[HttpGet]
[RestoreModelStateFromTempData]
public ActionResult Create(string uniqueUri)
{
    // get some stuff based on uniqueuri, set in ViewData.  
    return View();
}

[HttpPost]
[SetTempDataModelState]
public ActionResult Create(Review review)
{
    // validate review
    if (validatedOk)
    {
        return RedirectToAction("Details", new { postId = review.PostId});
    }  
    else
    {
        ModelState.AddModelError("ReviewErrors", "some error occured");
        return RedirectToAction("Create", new { uniqueUri = Request.RequestContext.RouteData.Values["uniqueUri"]});
    }   
}

Если вы также хотите передать модель в TempData (как предлагает bigb), вы все равно можете это сделать.


Спасибо. Мы реализовали нечто похожее на ваш подход. gist.github.com/ferventcoder/4735084
ferventcoder

Отличный ответ. Спасибо.
Марк Викери

3
Это решение является причиной, по которой я использую stackoverflow. Спасибо чувак!
jugg1es

@ asgeo1 - отличное решение, но я столкнулся с проблемой, используя его в сочетании с повторяющимися частичными представлениями, я разместил вопрос здесь: stackoverflow.com/questions/28372330/…
Джош

Прекрасный пример того, как простое решение сделать очень элегантным в духе MVC. Очень хорошо!
AHowgego,

7

Почему бы не создать частную функцию с логикой в ​​методе «Create» и вызвать этот метод как из метода Get, так и из метода Post и просто не вернуть View ().


На самом деле это то, чем я закончил - вы читаете мои мысли. +1 :)
RPM1984

1
Я тоже этим занимаюсь, только вместо частной функции я просто заставляю мой метод POST вызывать метод GET при ошибке (т.е. return Create(new { uniqueUri = ... });ваша логика остается СУХОЙ (как и при вызове RedirectToAction), но без проблем, связанных с перенаправлением, таких как потеря вашего ModelState.
Дэниел Лиуцци,

1
@DanielLiuzzi: это не изменит URL-адрес. Таким образом, вы заканчиваете URL чем-то вроде "/ controller / create /".
Скорунка Франтишек

@ SkorunkaFrantišek И в этом-то и дело. В вопросе говорится, что если возникает ошибка, мне нужно отобразить ту же страницу с ошибкой. В этом контексте вполне приемлемо (и предпочтительно ИМО), что URL-адрес НЕ изменяется, если отображается та же самая страница. Кроме того, одним из преимуществ этого подхода является то, что если рассматриваемая ошибка является не ошибкой проверки, а системной ошибкой (например, тайм-аут БД), он позволяет пользователю просто обновить страницу, чтобы повторно отправить форму.
Daniel Liuzzi


4

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

public class GetStuffBasedOnUniqueUriAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var filter = new GetStuffBasedOnUniqueUriFilter();

        filter.OnActionExecuting(filterContext);
    }
}


public class GetStuffBasedOnUniqueUriFilter : IActionFilter
{
    #region IActionFilter Members

    public void OnActionExecuted(ActionExecutedContext filterContext)
    {

    }

    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
        filterContext.Controller.ViewData["somekey"] = filterContext.RouteData.Values["uniqueUri"];
    }

    #endregion
}

Вот пример:

[HttpGet, GetStuffBasedOnUniqueUri]
public ActionResult Create()
{
    return View();
}

[HttpPost, GetStuffBasedOnUniqueUri]
public ActionResult Create(Review review)
{
    // validate review
    if (validatedOk)
    {
        return RedirectToAction("Details", new { postId = review.PostId });
    }

    ModelState.AddModelError("ReviewErrors", "some error occured");
    return View(review);
}

Как это плохая идея? Я думаю, что атрибут позволяет избежать необходимости использовать другое действие, потому что оба действия могут использовать атрибут для загрузки в ViewData.
CRice

1
Взгляните на шаблон Post / Redirect / Get: en.wikipedia.org/wiki/Post/Redirect/Get
DreamSonic

2
Обычно это используется после завершения проверки модели, чтобы предотвратить дальнейшие публикации в той же форме при обновлении. Но если в форме есть проблемы, то ее все равно нужно исправить и повторно опубликовать. Этот вопрос касается обработки ошибок модели.
CRice

Фильтры предназначены для многократно используемого кода действий, особенно полезны для помещения вещей в ViewData. TempData - это просто обходной путь.
CRice

1
@ppumkin, возможно, попробуйте опубликовать с помощью ajax, чтобы у вас не было проблем с восстановлением стороны сервера просмотра.
CRice

2

У меня есть метод, который добавляет состояние модели к временным данным. Затем у меня есть метод в моем базовом контроллере, который проверяет временные данные на наличие ошибок. Если они есть, он добавляет их обратно в ModelState.


1

Мой сценарий немного сложнее, поскольку я использую шаблон PRG, поэтому моя ViewModel («SummaryVM») находится в TempData, и мой экран Summary отображает его. На этой странице есть небольшая форма для отправки информации в другое действие. Сложность возникла из-за того, что пользователю требовалось отредактировать некоторые поля в SummaryVM на этой странице.

Summary.cshtml содержит сводку проверки, которая будет улавливать ошибки ModelState, которые мы создадим.

@Html.ValidationSummary()

Моя форма теперь должна отправить POST в действие HttpPost для Summary (). У меня есть еще одна очень маленькая ViewModel для представления отредактированных полей, и привязка модели предоставит мне их.

Новая форма:

@using (Html.BeginForm("Summary", "MyController", FormMethod.Post))
{
    @Html.Hidden("TelNo") @* // Javascript to update this *@

и действие ...

[HttpPost]
public ActionResult Summary(EditedItemsVM vm)

Здесь я провожу некоторую проверку и обнаруживаю некорректный ввод, поэтому мне нужно вернуться на страницу «Сводка» с ошибками. Для этого я использую TempData, которая выдержит перенаправление. Если с данными нет проблем, я заменяю объект SummaryVM копией (но с измененными полями, конечно), затем выполняю RedirectToAction ("NextAction");

// Telephone number wasn't in the right format
List<string> listOfErrors = new List<string>();
listOfErrors.Add("Telephone Number was not in the correct format. Value supplied was: " + vm.TelNo);
TempData["SummaryEditedErrors"] = listOfErrors;
return RedirectToAction("Summary");

Действие контроллера Summary, с которого все это начинается, ищет любые ошибки во временных данных и добавляет их в состояние модели.

[HttpGet]
[OutputCache(Duration = 0)]
public ActionResult Summary()
{
    // setup, including retrieval of the viewmodel from TempData...


    // And finally if we are coming back to this after a failed attempt to edit some of the fields on the page,
    // load the errors stored from TempData.
        List<string> editErrors = new List<string>();
        object errData = TempData["SummaryEditedErrors"];
        if (errData != null)
        {
            editErrors = (List<string>)errData;
            foreach(string err in editErrors)
            {
                // ValidationSummary() will see these
                ModelState.AddModelError("", err);
            }
        }

1

Microsoft удалила возможность хранить сложные типы данных в TempData, поэтому предыдущие ответы больше не работают; вы можете хранить только простые типы, такие как строки. Я изменил ответ @ asgeo1, чтобы он работал должным образом.

public class SetTempDataModelStateAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        base.OnActionExecuted(filterContext);

        var controller = filterContext.Controller as Controller;
        var modelState = controller?.ViewData.ModelState;
        if (modelState != null)
        {
            var listError = modelState.Where(x => x.Value.Errors.Any())
                .ToDictionary(m => m.Key, m => m.Value.Errors
                .Select(s => s.ErrorMessage)
                .FirstOrDefault(s => s != null));
            controller.TempData["KEY HERE"] = JsonConvert.SerializeObject(listError);
        }
    }
}


public class RestoreModelStateFromTempDataAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        base.OnActionExecuting(filterContext);

        var controller = filterContext.Controller as Controller;
        var tempData = controller?.TempData?.Keys;
        if (controller != null && tempData != null)
        {
            if (tempData.Contains("KEY HERE"))
            {
                var modelStateString = controller.TempData["KEY HERE"].ToString();
                var listError = JsonConvert.DeserializeObject<Dictionary<string, string>>(modelStateString);
                var modelState = new ModelStateDictionary();
                foreach (var item in listError)
                {
                    modelState.AddModelError(item.Key, item.Value ?? "");
                }

                controller.ViewData.ModelState.Merge(modelState);
            }
        }
    }
}

Отсюда вы можете просто добавить необходимую аннотацию данных в метод контроллера по мере необходимости.

[RestoreModelStateFromTempDataAttribute]
[HttpGet]
public async Task<IActionResult> MethodName()
{
}


[SetTempDataModelStateAttribute]
[HttpPost]
public async Task<IActionResult> MethodName()
{
    ModelState.AddModelError("KEY HERE", "ERROR HERE");
}

Прекрасно работает !. Отредактировал ответ, чтобы исправить небольшую ошибку скобок при вставке кода.
VDWWD

0

Я предпочитаю добавить в свою ViewModel метод, который заполняет значения по умолчанию:

public class RegisterViewModel
{
    public string FirstName { get; set; }
    public IList<Gender> Genders { get; set; }
    //Some other properties here ....
    //...
    //...

    ViewModelType PopulateDefaultViewData()
    {
        this.FirstName = "No body";
        this.Genders = new List<Gender>()
        {
            Gender.Male,
            Gender.Female
        };

        //Maybe other assinments here for other properties...
    }
}

Затем я вызываю его, когда мне нужны исходные данные следующим образом:

    [HttpGet]
    public async Task<IActionResult> Register()
    {
        var vm = new RegisterViewModel().PopulateDefaultViewValues();
        return View(vm);
    }

    [HttpPost]
    public async Task<IActionResult> Register(RegisterViewModel vm)
    {
        if (!ModelState.IsValid)
        {
            return View(vm.PopulateDefaultViewValues());
        }

        var user = await userService.RegisterAsync(
            email: vm.Email,
            password: vm.Password,
            firstName: vm.FirstName,
            lastName: vm.LastName,
            gender: vm.Gender,
            birthdate: vm.Birthdate);

        return Json("Registered successfully!");
    }

0

Я даю здесь только образец кода. В вашем viewModel вы можете добавить одно свойство типа "ModelStateDictionary" как

public ModelStateDictionary ModelStateErrors { get; set; }

и в вашем методе действия POST вы можете писать код напрямую, например

model.ModelStateErrors = ModelState; 

а затем назначьте эту модель Tempdata, как показано ниже

TempData["Model"] = model;

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

if (TempData["Model"] != null)
{
    viewModel = TempData["Model"] as ViewModel; //Your viewmodel class Type
    if(viewModel.ModelStateErrors != null && viewModel.ModelStateErrors.Count>0)
    {
        this.ViewData.ModelState.Merge(viewModel.ModelStateErrors);
    }
}

Вот и все. Для этого не нужно писать фильтры действий. Это так же просто, как и приведенный выше код, если вы хотите передать ошибки состояния модели другому представлению другого контроллера.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.