ASP.NET MVC - Làm thế nào để Bảo tồn lỗi ModelState trên RedirectToAction?


91

Tôi có hai phương pháp hành động sau (đơn giản hóa cho câu hỏi):

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

Vì vậy, nếu quá trình xác nhận vượt qua, tôi sẽ chuyển hướng đến một trang khác (xác nhận).

Nếu xảy ra lỗi, tôi cần hiển thị cùng một trang có lỗi.

Nếu tôi làm vậy return View(), lỗi sẽ được hiển thị, nhưng nếu tôi làm như vậy return RedirectToAction(như trên), nó sẽ mất các lỗi Model.

Tôi không ngạc nhiên về vấn đề này, chỉ tự hỏi làm thế nào các bạn xử lý điều này?

Tất nhiên, tôi có thể chỉ trả lại cùng một Chế độ xem thay vì chuyển hướng, nhưng tôi có logic trong phương thức "Tạo" điền dữ liệu chế độ xem, mà tôi sẽ phải sao chép.

Bất kỳ đề xuất?


10
Tôi giải quyết vấn đề này bằng cách không sử dụng mẫu Post-Redirect-Get cho các lỗi xác thực. Tôi chỉ sử dụng View (). Hoàn toàn hợp lệ để làm điều đó thay vì nhảy qua một loạt các vòng lặp - và chuyển hướng gây rối với lịch sử trình duyệt của bạn.
Jimmy Bogard

2
Và ngoài những gì @JimmyBogard đã nói, hãy trích xuất logic trong Createphương thức điền ViewData và gọi nó trong Createphương thức GET và cả trong nhánh xác thực không thành công trong Createphương thức POST.
Russ Cam

1
Nhất trí, né tránh vấn đề là một cách giải quyết. Tôi có một số logic để điền vào nội dung trong Createtầm nhìn của mình , tôi chỉ đặt nó trong một số phương pháp populateStuffmà tôi gọi trong cả phương thức và phương thức GETkhông thành công POST.
Francois Joly

12
@JimmyBogard Tôi không đồng ý, nếu bạn đăng lên một hành động và sau đó trả lại chế độ xem, bạn sẽ gặp phải vấn đề trong đó nếu người dùng nhấn refresh, họ sẽ nhận được cảnh báo về việc muốn bắt đầu lại bài đăng đó.
The Muffin Man,

Câu trả lời:


50

Bạn cần có cùng một trường hợp Reviewvề HttpGethành động của mình . Để làm điều đó, bạn nên lưu một đối tượng Review reviewtrong biến tạm thời trên HttpPosthành động của bạn và sau đó khôi phục nó khi HttpGethành động.

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

Nếu bạn muốn điều này hoạt động ngay cả khi trình duyệt được làm mới sau lần thực hiện đầu tiên của HttpGethành động, bạn có thể làm như sau:

  Review review = TempData["Review"] as Review;  
  TempData["Review"] = review;

Nếu không, đối tượng nút refresh reviewsẽ trống vì sẽ không có bất kỳ dữ liệu nào trong đó TempData["Review"].


2
Thông minh. Và một +1 lớn cho việc đề cập đến vấn đề làm mới. Đây là câu trả lời đầy đủ nhất vì vậy tôi sẽ chấp nhận nó, cảm ơn rất nhiều. :)
RPM1984

8
Điều này không thực sự trả lời câu hỏi trong tiêu đề. ModelState không được bảo toàn và có các phân nhánh như HtmlHelpers đầu vào không bảo toàn mục nhập của người dùng. Đây gần như là một cách giải quyết.
John Farrell

Tôi đã thực hiện những gì @Wim đề xuất trong câu trả lời của anh ấy.
RPM1984,

17
@jfar, tôi đồng ý, câu trả lời này không hoạt động và không tồn tại ModelState. Tuy nhiên, nếu bạn sửa đổi nó để nó làm điều gì đó giống như TempData["ModelState"] = ModelState; và khôi phục với ModelState.Merge((ModelStateDictionary)TempData["ModelState"]);, sau đó nó sẽ làm việc
asgeo1

1
Bạn có thể không chỉ return Create(uniqueUri)khi xác thực không thành công trên POST? Vì các giá trị ModelState được ưu tiên hơn ViewModel được chuyển vào chế độ xem, dữ liệu đã đăng sẽ vẫn còn.
ajbeaven

83

Tôi đã phải tự mình giải quyết vấn đề này hôm nay và bắt gặp câu hỏi này.

Một số câu trả lời hữu ích (sử dụng TempData), nhưng không thực sự trả lời câu hỏi trong tầm tay.

Lời khuyên tốt nhất tôi tìm thấy là trên bài đăng blog này:

http://www.jefclaes.be/2012/06/persisting-model-state-when-using-prg.html

Về cơ bản, sử dụng TempData để lưu và khôi phục đối tượng ModelState. Tuy nhiên, sẽ gọn gàng hơn rất nhiều nếu bạn tóm tắt điều này thành các thuộc tính.

Ví dụ

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

Sau đó, theo ví dụ của bạn, bạn có thể lưu / khôi phục ModelState như sau:

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

Nếu bạn cũng muốn chuyển mô hình đó trong TempData (như bigb đề xuất) thì bạn vẫn có thể làm điều đó.


Cảm ơn bạn. Chúng tôi đã triển khai một cái gì đó tương tự như cách tiếp cận của bạn. gist.github.com/ferventcoder/4735084
ferventcoder

Câu trả lời chính xác. Cảm ơn.
Mark Vickery

3
Giải pháp này là lý do tôi sử dụng stackoverflow. Cảm ơn anh bạn!
jugg1es

@ asgeo1 - giải pháp tuyệt vời, nhưng tôi đã gặp sự cố khi sử dụng nó kết hợp với việc lặp lại Chế độ xem từng phần, tôi đã đăng câu hỏi ở đây: stackoverflow.com/questions/28372330/…
Josh

Một ví dụ đáng yêu về việc sử dụng các giải pháp đơn giản và làm cho nó rất thanh lịch, theo tinh thần của MVC. Rất đẹp!
AHowgego

7

Tại sao không tạo một hàm private với logic trong phương thức "Create" và gọi phương thức này từ cả phương thức Get và Post và chỉ cần trả về View ().


Đây thực sự là những gì tôi đã làm - bạn đọc được suy nghĩ của tôi. +1 :)
RPM1984

1
Đây là những gì tôi cũng làm, chỉ thay vì có một chức năng riêng tư, tôi chỉ đơn giản là phương thức POST của tôi gọi phương thức GET khi bị lỗi (tức là return Create(new { uniqueUri = ... });. Logic của bạn vẫn KHÔ (giống như gọi RedirectToAction), nhưng không có vấn đề do chuyển hướng, chẳng hạn như mất ModelState của bạn.
Daniel Liuzzi

1
@DanielLiuzzi: làm theo cách đó sẽ không thay đổi URL. Vì vậy, bạn kết thúc bằng url giống như "/ controller / create /".
Skorunka František

@ SkorunkaFrantišek Và đó chính xác là vấn đề. Câu hỏi nêu rõ Nếu có lỗi xảy ra, tôi cần hiển thị cùng một trang có lỗi. Trong ngữ cảnh này, hoàn toàn có thể chấp nhận được (và tốt hơn là IMO) rằng URL KHÔNG thay đổi nếu cùng một trang được hiển thị. Ngoài ra, một ưu điểm của phương pháp này là nếu lỗi được đề cập không phải là lỗi xác thực mà là lỗi hệ thống (ví dụ: thời gian chờ của DB), nó cho phép người dùng chỉ cần làm mới trang để gửi lại biểu mẫu.
Daniel Liuzzi

4

tôi có thể sử dụng TempData["Errors"]

TempData được chuyển qua các hành động bảo quản dữ liệu 1 lần.


4

Tôi khuyên bạn nên trả lại chế độ xem và tránh trùng lặp thông qua một thuộc tính trên hành động. Đây là một ví dụ về cách điền để xem dữ liệu. Bạn có thể làm điều gì đó tương tự với logic phương thức tạo của bạn.

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
}

Đây là một ví dụ:

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

Làm thế nào đây là một ý tưởng tồi? Tôi nghĩ rằng thuộc tính này tránh được sự cần thiết phải sử dụng một hành động khác vì cả hai hành động đều có thể sử dụng thuộc tính để tải vào ViewData.
CRice

1
Hãy xem ở bài viết / Redirect / Nhận mẫu: en.wikipedia.org/wiki/Post/Redirect/Get
DreamSonic

2
Điều đó thường được sử dụng sau khi xác thực mô hình được thỏa mãn, để ngăn chặn các bài đăng tiếp theo cùng một biểu mẫu khi làm mới. Nhưng nếu biểu mẫu có vấn đề, thì dù sao nó cũng cần được sửa chữa và đăng lại. Câu hỏi này đề cập đến việc xử lý các lỗi mô hình.
CRice

Bộ lọc dành cho mã có thể sử dụng lại trên các hành động, đặc biệt hữu ích để đưa mọi thứ vào ViewData. TempData chỉ là một giải pháp thay thế.
CRice

1
@ppumkin có thể thử đăng bằng ajax để bạn không gặp khó khăn khi xây dựng lại phía máy chủ chế độ xem của mình.
CRice

2

Tôi có một phương pháp thêm trạng thái mô hình vào dữ liệu tạm thời. Sau đó, tôi có một phương thức trong bộ điều khiển cơ sở của mình để kiểm tra dữ liệu tạm thời cho bất kỳ lỗi nào. Nếu có chúng, nó sẽ thêm chúng trở lại ModelState.


1

Kịch bản của tôi phức tạp hơn một chút vì tôi đang sử dụng mẫu PRG nên ViewModel ("SummaryVM") của tôi ở trong TempData và màn hình Tóm tắt của tôi hiển thị nó. Có một biểu mẫu nhỏ trên trang này để ĐĂNG một số thông tin cho một Hành động khác. Sự phức tạp đến từ yêu cầu người dùng chỉnh sửa một số trường trong SummaryVM trên trang này.

Summary.cshtml có tóm tắt xác thực sẽ bắt lỗi ModelState mà chúng tôi sẽ tạo.

@Html.ValidationSummary()

Biểu mẫu của tôi bây giờ cần ĐĂNG lên hành động HttpPost cho Summary (). Tôi có một ViewModel rất nhỏ khác để đại diện cho các trường đã chỉnh sửa và việc lập mô hình sẽ giúp tôi nhận được những điều này.

Hình thức mới:

@using (Html.BeginForm("Summary", "MyController", FormMethod.Post))
{
    @Html.Hidden("TelNo") @* // Javascript to update this *@

và hành động ...

[HttpPost]
public ActionResult Summary(EditedItemsVM vm)

Tại đây, tôi thực hiện một số xác thực và tôi phát hiện một số đầu vào không hợp lệ, vì vậy tôi cần quay lại trang Tóm tắt có lỗi. Đối với điều này, tôi sử dụng TempData, nó sẽ tồn tại khi chuyển hướng. Nếu không có vấn đề gì với dữ liệu, tôi thay thế đối tượng SummaryVM bằng một bản sao (nhưng với các trường đã chỉnh sửa thì tất nhiên đã thay đổi) rồi thực hiện 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");

Hành động của bộ điều khiển Tóm tắt, nơi tất cả điều này bắt đầu, tìm kiếm bất kỳ lỗi nào trong dữ liệu tạm thời và thêm chúng vào 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 đã loại bỏ khả năng lưu trữ các kiểu dữ liệu phức tạp trong TempData, do đó các câu trả lời trước đó không còn hoạt động; bạn chỉ có thể lưu trữ các loại đơn giản như chuỗi. Tôi đã thay đổi câu trả lời của @ asgeo1 để hoạt động như mong đợi.

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

Từ đây, bạn có thể chỉ cần thêm chú thích dữ liệu bắt buộc vào phương thức bộ điều khiển nếu cần.

[RestoreModelStateFromTempDataAttribute]
[HttpGet]
public async Task<IActionResult> MethodName()
{
}


[SetTempDataModelStateAttribute]
[HttpPost]
public async Task<IActionResult> MethodName()
{
    ModelState.AddModelError("KEY HERE", "ERROR HERE");
}

Hoạt động hoàn hảo !. Đã chỉnh sửa câu trả lời để sửa lỗi dấu ngoặc nhỏ khi dán mã.
VDWWD

0

Tôi muốn thêm một phương thức vào ViewModel của mình để điền các giá trị mặc định:

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...
    }
}

Sau đó, tôi gọi nó khi nào tôi cần dữ liệu gốc như thế này:

    [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

Tôi chỉ cung cấp mã mẫu ở đây Trong viewModel của bạn, bạn có thể thêm một thuộc tính kiểu "ModelStateDictionary" là

public ModelStateDictionary ModelStateErrors { get; set; }

và trong phần hướng dẫn hành động POST của bạn, bạn có thể viết mã trực tiếp như

model.ModelStateErrors = ModelState; 

và sau đó gán mô hình này cho Tempdata như bên dưới

TempData["Model"] = model;

và khi bạn chuyển hướng đến phương thức hành động của bộ điều khiển khác thì trong bộ điều khiển, bạn phải đọc giá trị 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);
    }
}

Đó là nó. Bạn không cần phải viết các bộ lọc hành động cho việc này. Điều này đơn giản như đoạn mã trên nếu bạn muốn nhận lỗi trạng thái Mô hình sang một chế độ xem khác của bộ điều khiển khác.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.