ASP.NET MVC - วิธีการรักษาข้อผิดพลาด ModelState ข้าม RedirectToAction?


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(ตามด้านบน) จะสูญเสียข้อผิดพลาดของโมเดล

ฉันไม่แปลกใจกับปัญหานี้แค่สงสัยว่าพวกคุณจัดการเรื่องนี้อย่างไร?

แน่นอนว่าฉันสามารถคืนมุมมองเดียวกันแทนการเปลี่ยนเส้นทางได้ แต่ฉันมีตรรกะในวิธีการ "สร้าง" ซึ่งจะเติมข้อมูลมุมมองซึ่งฉันต้องทำซ้ำ

ข้อเสนอแนะใด ๆ ?


10
ฉันแก้ปัญหานี้โดยไม่ใช้รูปแบบ Post-Redirect-Get สำหรับข้อผิดพลาดในการตรวจสอบความถูกต้อง ฉันแค่ใช้ View () มันถูกต้องอย่างสมบูรณ์ที่จะทำเช่นนั้นแทนที่จะกระโดดข้ามห่วง - และเปลี่ยนเส้นทางไปยุ่งกับประวัติเบราว์เซอร์ของคุณ
Jimmy Bogard

2
และนอกเหนือจากสิ่งที่ @JimmyBogard กล่าวแล้วให้แยกตรรกะในCreateวิธีการที่เติมข้อมูล ViewData และเรียกมันในCreateเมธอด GET และในสาขาการตรวจสอบที่ล้มเหลวในCreateเมธอด POST
Russ Cam

1
การหลีกเลี่ยงปัญหาเป็นวิธีหนึ่งในการแก้ปัญหา ผมมีเหตุผลบางอย่างที่จะเติมสิ่งที่ฉันในCreateมุมมองของฉันเพียงแค่ใส่ไว้ในวิธีการบางอย่างpopulateStuffที่ผมเรียกทั้งในและล้มเหลวGET POST
Francois Joly

12
@JimmyBogard ฉันไม่เห็นด้วยถ้าคุณโพสต์การดำเนินการแล้วส่งคืนมุมมองที่คุณพบในปัญหาที่หากผู้ใช้กดรีเฟรชพวกเขาจะได้รับคำเตือนว่าต้องการเริ่มโพสต์นั้นอีกครั้ง
The 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 ไม่สงวนรายการผู้ใช้ นี่เป็นวิธีแก้ปัญหาเกือบทั้งหมด
John Farrell

ฉันลงเอยด้วยการทำตามที่ @Wim แนะนำในคำตอบของเขา
RPM1984

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

หากคุณต้องการส่งโมเดลไปพร้อมกับ TempData (ตามที่ bigb แนะนำ) คุณก็ยังสามารถทำได้เช่นกัน


ขอบคุณ. เรานำสิ่งที่คล้ายกับแนวทางของคุณไปใช้ gist.github.com/ferventcoder/4735084
ferventcoder

@ asgeo1 - วิธีแก้ปัญหาที่ยอดเยี่ยม แต่ฉันพบปัญหาในการใช้งานร่วมกับการดูบางส่วนซ้ำฉันโพสต์คำถามที่นี่: stackoverflow.com/questions/28372330/…
Josh

คำเตือน - หากมีการแสดงเพจทั้งหมดในคำขอเดียว (และไม่แยกย่อยผ่าน AJAX) คุณกำลังขอปัญหาในการใช้โซลูชันนี้เนื่องจาก TempData จะถูกเก็บไว้จนถึงคำขอถัดไป ตัวอย่างเช่นคุณป้อนเกณฑ์การค้นหาลงในหน้าเดียวจากนั้น PRG ไปที่ผลการค้นหาจากนั้นคลิกลิงก์เพื่อกลับไปที่หน้าการค้นหาโดยตรงค่าการค้นหาเดิมจะถูกเติมใหม่ พฤติกรรมแปลก ๆ และบางครั้งก็ยากที่จะสร้างซ้ำก็ปรากฏขึ้นเช่นกัน
PJ7

ฉันไม่สามารถทำงานนี้ได้จนกว่าฉันจะรู้ว่ารหัสเซสชันยังคงเปลี่ยนแปลงอยู่ สิ่งนี้ช่วยฉันแก้ปัญหานั้น: stackoverflow.com/a/5835631/1185136
Rudey

ถาม: อะไรคือพฤติกรรมNextRequestและTempDataเมื่อมีหลายแท็บเบราว์เซอร์ที่สร้างคำขอ (หลายรายการ / พร้อมกัน)
แดน

7

ทำไมไม่สร้างฟังก์ชันส่วนตัวด้วยตรรกะในเมธอด "สร้าง" และเรียกใช้เมธอดนี้จากทั้งเมธอด Get และ the Post และเพียงแค่ส่งกลับ View ()


1
นี่คือสิ่งที่ฉันทำเช่นกันแทนที่จะมีฟังก์ชันส่วนตัวฉันเพียงแค่มีเมธอด POST ของฉันเรียกเมธอด GET เมื่อเกิดข้อผิดพลาด (เช่นreturn Create(new { uniqueUri = ... });ตรรกะของคุณยังคงแห้ง (เหมือนการโทรRedirectToAction) แต่ไม่มีปัญหาที่เกิดจากการเปลี่ยนเส้นทางเช่น สูญเสีย ModelState ของคุณ
Daniel Liuzzi

1
@DanielLiuzzi: การทำเช่นนั้นจะไม่เปลี่ยน URL คุณจึงลงท้ายด้วย url เช่น "/ controller / create /"
Skorunka František

@ SkorunkaFrantišekและนั่นคือประเด็น คำถามระบุว่าหากมีข้อผิดพลาดเกิดขึ้นฉันต้องแสดงหน้าเดียวกันพร้อมกับข้อผิดพลาด ในบริบทนี้เป็นที่ยอมรับอย่างสมบูรณ์ (และเป็นที่ต้องการของ IMO) ว่า URL จะไม่เปลี่ยนแปลงหากมีการแสดงเพจเดียวกัน นอกจากนี้ข้อดีอย่างหนึ่งของวิธีนี้ก็คือหากข้อผิดพลาดที่เป็นปัญหาไม่ใช่ข้อผิดพลาดในการตรวจสอบความถูกต้อง แต่เป็นข้อผิดพลาดของระบบ (เช่นการหมดเวลาของ DB) จะช่วยให้ผู้ใช้เพียงรีเฟรชเพจเพื่อส่งแบบฟอร์มอีกครั้ง
Daniel Liuzzi

4

ฉันสามารถใช้ TempData["Errors"]

TempData จะถูกส่งผ่านการดำเนินการที่เก็บรักษาข้อมูล 1 ครั้ง


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

1
โปรดดูรูปแบบ Post / Redirect / Get: 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 ขนาดเล็กมากอีกตัวหนึ่งเพื่อแสดงถึงฟิลด์ที่แก้ไขและ 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");

การดำเนินการตัวควบคุมสรุปซึ่งทั้งหมดนี้เริ่มต้นขึ้นจะค้นหาข้อผิดพลาดใน 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);
            }
        }

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.