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

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

  1. Реализуйте два атрибута. Вы должны явно указать тип для контроллера (filterContext.Controller как Controller), потому что по умолчанию это тип объекта.
  2. Явно реализуйте сериализацию ModelState из этой статьи https://andrewlock.net/post-redirect-get-using-tempdata-in-asp-net-core/
  3. Если 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]

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