ASP.NET MVC-RedirectToAction에서 ModelState 오류를 보존하는 방법?


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 ()를 사용합니다. 많은 수고를 뛰어 넘는 대신에 그렇게하는 것이 완벽하게 타당합니다.
Jimmy Bogard 2013

2
그리고 @JimmyBogard가 말한 것 외에도 CreateViewData를 채우는 메서드 에서 논리를 추출 하고 CreateGET 메서드와 CreatePOST 메서드 의 실패한 유효성 검사 분기에서 호출합니다 .
Russ Cam

1
동의합니다. 문제를 피하는 것이 문제를 해결하는 한 가지 방법입니다. 내 Create뷰 에 항목을 채우는 논리가 있습니다. populateStuffthe GET및 fail 모두에서 호출하는 일부 메서드에 넣었습니다 POST.
Francois Joly 2013

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 2011 년

8
이것은 제목의 질문에 실제로 답하지 않습니다. ModelState는 보존되지 않으며 사용자 항목을 보존하지 않는 입력 HtmlHelpers와 같은 결과가 있습니다. 이것은 거의 해결 방법입니다.
John Farrell 2011 년

나는 @Wim이 그의 대답에서 제안한 것을 끝내 었습니다.
RPM1984 2011 년

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

(bigb가 제안한대로) TempData에서 모델을 전달하려는 경우에도 그렇게 할 수 있습니다.


감사합니다. 우리는 귀하의 접근 방식과 유사한 것을 구현했습니다. gist.github.com/ferventcoder/4735084
ferventcoder

좋은 대답입니다. 감사.
Mark Vickery 2014 년

3
이 솔루션은 내가 stackoverflow를 사용하는 이유입니다. 감사합니다!
jugg1es 2014 년

@ asgeo1-훌륭한 솔루션이지만 부분보기를 반복하여 사용하는 데 문제가 발생했습니다. 여기에 질문을 게시했습니다. stackoverflow.com/questions/28372330/…
Josh

MVC의 정신으로 간단한 솔루션을 가져와 매우 우아하게 만드는 멋진 예입니다. 아주 좋아요!
AHowgego

7

"Create"메서드의 논리를 사용하여 개인 함수를 만들고 Get 및 Post 메서드에서이 메서드를 호출하고 View ()를 반환하는 것이 어떻습니까?


이것은 실제로 내가 한 일입니다. 당신은 내 마음을 읽었습니다. +1 :)
RPM1984 2011 년

1
이것은 내가하는 일이기도합니다. 개인 기능을 갖는 대신에 오류가 발생하면 POST 메서드가 GET 메서드를 호출하도록합니다 (예 : return Create(new { uniqueUri = ... });. 귀하의 논리는 DRY 상태로 유지됩니다 (를 호출하는 것과 유사 함 RedirectToAction). 당신의 ModelState을 잃고.
다니엘 Liuzzi

1
@DanielLiuzzi : 이렇게하면 URL이 변경되지 않습니다. 따라서 "/ controller / create /"와 같은 URL로 끝납니다.
Skorunka František 2012-06-09

@ SkorunkaFrantišek 그리고 그게 바로 요점입니다. 오류가 발생하면 오류가있는 동일한 페이지를 표시해야한다는 질문이 있습니다 . 이러한 맥락에서 동일한 페이지가 표시되는 경우 URL이 변경되지 않는 것이 완벽하게 허용됩니다 (그리고 선호되는 IMO). 또한이 접근 방식의 장점 중 하나는 문제의 오류가 유효성 검사 오류가 아니라 시스템 오류 (예 : DB 시간 초과) 인 경우 사용자가 페이지를 새로 고쳐 양식을 다시 제출할 수 있다는 것입니다.
Daniel Liuzzi 2011 년

4

나는 사용할 수있다 TempData["Errors"]

TempData는 데이터를 한 번 보존하는 모든 작업에 전달됩니다.


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 2011 년

1
Post / Redirect / Get 패턴을 살펴보십시오 : en.wikipedia.org/wiki/Post/Redirect/Get
DreamSonic 2011 년

2
일반적으로 모델 유효성 검사가 충족 된 후 새로 고침시 동일한 양식에 대한 추가 게시를 방지하기 위해 사용됩니다. 그러나 양식에 문제가있는 경우 어쨌든 수정하고 다시 게시해야합니다. 이 질문은 모델 오류 처리를 다룹니다.
CRice 2011 년

필터는 작업에 대한 재사용 가능한 코드 용이며 특히 ViewData에 항목을 넣을 때 유용합니다. TempData는 해결 방법 일뿐입니다.
CRice

1
@ppumkin은 뷰 서버 측을 재 구축하는 데 어려움을 겪지 않도록 ajax로 게시를 시도 할 수 있습니다.
CRice

2

임시 데이터에 모델 상태를 추가하는 방법이 있습니다. 그런 다음 임시 데이터에서 오류를 확인하는 기본 컨트롤러에 메서드가 있습니다. 그것들이 있으면 ModelState에 다시 추가합니다.


1

내 시나리오는 PRG 패턴을 사용하고 있으므로 내 ViewModel ( "SummaryVM")이 TempData에 있고 요약 화면에 표시되므로 조금 더 복잡합니다. 이 페이지에는 일부 정보를 다른 작업에 게시하는 작은 양식이 있습니다. 이 문제는 사용자가이 페이지의 SummaryVM에서 일부 필드를 편집해야한다는 요구 사항에서 비롯되었습니다.

Summary.cshtml에는 생성 할 ModelState 오류를 포착하는 유효성 검사 요약이 있습니다.

@Html.ValidationSummary()

내 양식은 이제 Summary ()에 대한 HttpPost 작업에 POST해야합니다. 편집 된 필드를 나타내는 또 다른 매우 작은 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");

이 모든 것이 시작되는 요약 컨트롤러 작업은 임시 데이터에서 오류를 찾아서 모델 상태에 추가합니다.

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

그게 다야. 이를 위해 액션 필터를 작성할 필요가 없습니다. 다른 컨트롤러의 다른 뷰에 모델 상태 오류를 가져 오려면 위 코드처럼 간단합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.