103

我有以下两种操作方法(为问题而简化):

[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(如上所述),它会丢失模型错误。

我对这个问题并不感到惊讶,只是想知道你们如何处理这个问题?

我当然可以只返回相同的视图而不是重定向,但是我在填充视图数据的“创建”方法中有逻辑,我必须复制这些数据。

有什么建议么?

4

11 回答 11

85

我今天不得不自己解决这个问题,并遇到了这个问题。

有些答案很有用(使用 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 建议的那样),那么您仍然可以这样做。

于 2012-08-19T04:30:21.433 回答
52

您需要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"].

于 2011-01-10T00:54:11.423 回答
7

为什么不使用“Create”方法中的逻辑创建一个私有函数,并从 Get 和 Post 方法调用此方法,然后返回 View()。

于 2011-01-10T11:11:43.620 回答
5

我可以使用TempData["Errors"]

TempData 在保存数据的操作中传递 1 次。

于 2011-01-10T00:52:42.670 回答
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);
}
于 2011-01-10T00:56:15.990 回答
2

我有一种将模型状态添加到临时数据的方法。然后,我的基本控制器中有一个方法可以检查临时数据是否有任何错误。如果它有它们,它会将它们添加回 ModelState。

于 2011-01-10T01:27:14.773 回答
2

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");
}
于 2018-09-07T16:30:40.490 回答
1

我的场景有点复杂,因为我使用的是 PRG 模式,所以我的 ViewModel(“SummaryVM”)在 TempData 中,我的摘要屏幕显示它。此页面上有一个小表单,用于将一些信息发布到另一个操作。复杂性来自要求用户在此页面上编辑 SummaryVM 中的某些字段。

Summary.cshtml 具有验证摘要,它将捕获我们将创建的 ModelState 错误。

@Html.ValidationSummary()

我的表单现在需要 POST 到 Summary() 的 HttpPost 操作。我有另一个非常小的 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 控制器动作,所有这一切都从这里开始,查找临时数据中的任何错误并将它们添加到模型状态。

[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);
            }
        }
于 2015-12-10T17:22:30.650 回答
1
    public class RestoreModelStateFromTempDataAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var controller = filterContext.Controller as Controller;
            if (controller.TempData.ContainsKey("ModelState"))
            {
                var modelState = ModelStateHelpers.DeserialiseModelState(controller.TempData["ModelState"].ToString());
                controller.ViewData.ModelState.Merge(modelState);
            }
            base.OnActionExecuting(filterContext);
        }
    }
    public class SetTempDataModelStateAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            var controller = filterContext.Controller as Controller;
            controller.TempData["ModelState"] = ModelStateHelpers.SerialiseModelState(controller.ViewData.ModelState);
            base.OnActionExecuted(filterContext);
        }
    }

当我解决一些问题时,我遇到了很多不明显的障碍。我会一步一步说明一切。我的评论将部分重复当前分支的答案

  1. 实现两个属性。您必须明确指定控制器的类型(filterContext.Controller 作为控制器),因为默认是对象类型。
  2. 从这篇文章中显式实现序列化 ModelState https://andrewlock.net/post-redirect-get-using-tempdata-in-asp-net-core/
  3. 检查 startup.cs 中的实现缓存时,如果目标操作中的 TempData 为空。您需要添加 memoryCache 或 SqlServerCache 或另一个https://stackoverflow.com/a/41500275/11903993
于 2021-07-29T21:50:40.043 回答
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);
    }
}

而已。您不必为此编写操作过滤器。如果您想将模型状态错误发送到另一个控制器的另一个视图,这就像上面的代码一样简单。

于 2020-08-31T14:18:00.240 回答
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!");
    }
于 2017-12-23T00:13:35.400 回答