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 (как и выше), он теряет ошибки модели.

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

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

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

9 75

9 ответов:

вы должны иметь тот же экземпляр Review на HttpGet действие. Для этого вы должны сохранить объект Review review в переменной temp на вашем 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 (как предложил bigb), вы все равно можете это сделать.

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

Я мог бы использовать TempData["Errors"]

TempData передаются через действия, сохраняющие данные 1 раз.

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

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.

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

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

@Html.ValidationSummary()

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

новая форма:

@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 controller, где все это начинается, ищет любые ошибки в tempdata и добавляет их в modelstate.

[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, который заполняет значения по умолчанию:

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

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