ASP.NET MVC - Как сохранить ошибки ModelState через RedirectToAction?
У меня есть следующие два метода действий (упрощенный для вопроса):
[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
(как указано выше), он теряет ошибки модели.
Я не удивлен этой проблемой, просто интересно, как вы, ребята, справляетесь с этим?
Конечно, я мог бы просто вернуть тот же самый вид вместо перенаправления, но у меня есть логика в методе "Создать", который заполняет данные представления, которые мне пришлось бы дублировать.
Какие-либо предложения?
12 ответов
Вы должны иметь тот же экземпляр 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 you 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"]
,
Я должен был решить эту проблему сегодня сам, и столкнулся с этим вопросом.
Некоторые ответы полезны (с использованием 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 (как предложил Бигб), вы все равно можете это сделать.
Почему бы не создать приватную функцию с логикой в методе "Создать" и вызвать этот метод как из метода Get, так и из метода Post и просто вернуть View().
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");
}
Я мог бы использовать TempData["Errors"]
TempData передается через действия, сохраняющие данные 1 раз.
Я предлагаю вам вернуть представление и избежать дублирования с помощью атрибута в действии. Вот пример заполнения для просмотра данных. Вы можете сделать что-то подобное с вашей логикой метода создания.
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);
}
У меня есть метод, который добавляет состояние модели к временным данным. Затем в моем базовом контроллере есть метод, который проверяет временные данные на наличие ошибок. Если он есть, он добавляет их обратно в ModelState.
public class RestoreModelStateFromTempDataAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var controller = filterContext.Controller as Controller;
if (controller.TempData.ContainsKey("ModelState"))
{
var modelState = ModelStateHelpers.DeserialiseModelState(controller.TempData["ModelState"].ToString());
controller.ViewData.ModelState.Merge(modelState);
}
base.OnActionExecuting(filterContext);
}
}
public class SetTempDataModelStateAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
var controller = filterContext.Controller as Controller;
controller.TempData["ModelState"] = ModelStateHelpers.SerialiseModelState(controller.ViewData.ModelState);
base.OnActionExecuted(filterContext);
}
}
Когда я решал какую-то проблему, я сталкивался с множеством неочевидных препятствий. Все укажу пошагово. Мои комментарии будут частично дублировать ответы из текущей ветки
- Реализуйте два атрибута. Вы должны явно указать тип для контроллера (filterContext.Controller как Controller), потому что по умолчанию это тип объекта.
- Явно реализуйте сериализацию ModelState из этой статьи https://andrewlock.net/post-redirect-get-using-tempdata-in-asp-net-core/
- Если TempData пуста в действии назначения при проверке кеша реализации в startup.cs. Вам нужно добавить memoryCache или SqlServerCache или другой /questions/23808614/peredacha-tempdata-s-redirecttoaction/23808626#23808626
Мой сценарий немного сложнее, так как я использую шаблон 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, где все это начинается, ищет любые ошибки в tempdata и добавляет их в состояние модели.
[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);
}
}
Я даю здесь только образец кода. В вашем 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);
}
}
Вот и все. Для этого не нужно писать фильтры действий. Это так же просто, как и приведенный выше код, если вы хотите передавать ошибки состояния модели в другое представление другого контроллера.
Я предпочитаю добавить метод к моей 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!");
}
Я делаю этот атрибут
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
namespace Network.Utilites
{
public class PreserveModelStateAttribute : ActionFilterAttribute
{
private const string KeyListKey = "__preserveModelState_keys";
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
var controller = filterContext.Controller as Controller;
if (controller != null && !controller.ViewData.ModelState.IsValid)
{
var keys = controller.ViewData.ModelState.Keys.ToList();
controller.TempData[KeyListKey] = keys;
controller.TempData[ModelStateDictionaryTempDataKey()] = controller.ViewData.ModelState;
}
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var controller = filterContext.Controller as Controller;
if (controller != null && controller.TempData.ContainsKey(KeyListKey))
{
var keys = (IEnumerable<string>)controller.TempData[KeyListKey];
var tempDataModelState = (ModelStateDictionary)controller.TempData[ModelStateDictionaryTempDataKey()];
foreach (var key in keys)
{
if (!controller.ViewData.ModelState.ContainsKey(key))
{
controller.ViewData.ModelState.Add(key, tempDataModelState[key]);
}
else
{
foreach (var error in tempDataModelState[key].Errors)
{
controller.ViewData.ModelState[key].Errors.Add(error);
}
}
}
}
}
private static string ModelStateDictionaryTempDataKey()
{
return "__preserveModelState_modelState";
}
}
}
но вы используете этот атрибут для обоих действий [PreserveModelState]