ASP.NET MVC - Як зберегти помилки ModelState через RedirectToAction?


91

У мене є такі два методи дії (спрощені для запитань):

[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(як зазначено вище), вона втрачає помилки моделі.

Я не здивований проблемою, просто цікаво, як ви, хлопці, вирішуєте це?

Звичайно, я міг би просто повернути той самий подання замість перенаправлення, але у мене є логіка в методі "Створити", який заповнює дані подання, які мені довелося б дублювати.

Будь-які пропозиції?


10
Я вирішую цю проблему, не використовуючи шаблон Post-Redirect-Get для помилок перевірки. Я просто використовую View (). Це цілком справедливо робити це, замість того, щоб стрибати через купу обручів - і перенаправляти помилки з історією вашого браузера.
Jimmy Bogard

2
І на додаток до сказаного @JimmyBogard, витягніть логіку в Createметоді, який заповнює ViewData, і викличте його у Createметоді GET, а також у гілці невдалої перевірки в Createметоді POST.
Russ Cam

1
Погоджено, уникнення проблеми - це один із способів її вирішення. У мене є деяка логіка, щоб заповнити речі на мій Createпогляд, я просто вклав це в якийсь метод, populateStuffякий я називаю як GETі помилковим POST.
Франсуа Джолі

12
@JimmyBogard.
Muffin Man

Відповіді:


50

Вам потрібно мати той самий екземпляр для 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 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"]);
робив

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

83

Мені довелося сьогодні вирішити цю проблему самостійно, і я натрапив на це питання.

Деякі відповіді корисні (за допомогою 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

Чудова відповідь. Дякую.
Mark Vickery

3
Це рішення є причиною того, що я використовую stackoverflow. Дякую людино!
jugg1es

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

Прекрасний приклад того, як взяти просте рішення і зробити його дуже елегантним, у дусі MVC. Дуже хороша!
AHowgego

7

Чому б не створити приватну функцію з логікою методу "Створити" і викликати цей метод як з методів Get, так і з Post, а просто повернути View ().


Це насправді те, що я в підсумку зробив - ти читаєш мої думки. +1 :)
RPM1984,

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

1
@DanielLiuzzi: якщо зробити це таким чином, URL-адреса не зміниться. Отже, ви закінчуєте URL-адресою щось на зразок "/ controller / create /".
Скорунка Франтішек

@ SkorunkaFrantišek І саме в цьому суть. Запитання стверджує, що якщо виникає помилка, мені потрібно відобразити ту саму сторінку з помилкою. У цьому контексті цілком прийнятно (і бажано IMO), щоб URL-адреса НЕ змінювалась, якщо відображається одна і та ж сторінка. Крім того, однією перевагою цього підходу є те, що якщо помилка, про яку йдеться, не є помилкою перевірки, а системною помилкою (наприклад, час очікування БД), це дозволяє користувачеві просто оновити сторінку, щоб повторно надіслати форму.
Даніель Люцці


4

Я пропоную вам повернути подання та уникати дублювання через атрибут дії. Ось приклад заповнення для перегляду даних. Ви можете зробити щось подібне за допомогою логіки методу 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);
}

Як це погана ідея? Я думаю, що атрибут уникає необхідності використовувати іншу дію, оскільки обидві дії можуть використовувати атрибут для завантаження до ViewData.
CRice

1
Будь ласка, подивіться на Post / Redirect / Get pattern: en.wikipedia.org/wiki/Post/Redirect/Get
DreamSonic

2
Це зазвичай використовується після того, як перевірка моделі виконана, щоб запобігти подальшим публікаціям у тій самій формі під час оновлення. Але якщо у формі є проблеми, її все одно потрібно виправити та повторно опублікувати. Це питання стосується обробки помилок моделі.
CRice

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

1
@ppumkin, можливо, спробуйте розмістити повідомлення за допомогою ajax, щоб вам не було важко перебудувати сторону сервера перегляду.
CRice

2

У мене є метод, який додає стан моделі до тимчасових даних. Потім у моєму базовому контролері є метод, який перевіряє тимчасові дані на наявність помилок. Якщо вони є, вони додають їх назад до ModelState.


1

Мій сценарій дещо складніший, оскільки я використовую шаблон PRG, тому мій ViewModel ("SummaryVM") знаходиться в TempData, і на екрані "Підсумок" він відображається. На цій сторінці є невелика форма для розміщення певної інформації в іншій дії. Складність пов’язана з вимогою до користувача редагувати деякі поля в 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);
            }
        }

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 дії menthod ви можете написати код безпосередньо, як

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.