Mam następujące dwie metody działania (uproszczone do pytania):
[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"]});
}
}
Jeśli więc walidacja przejdzie pomyślnie, przekierowuję na inną stronę (potwierdzenie).
Jeśli wystąpi błąd, muszę wyświetlić tę samą stronę z błędem.
Jeśli to zrobię return View()
, błąd jest wyświetlany, ale jeśli to zrobię return RedirectToAction
(jak powyżej), powoduje utratę błędów modelu.
Nie jestem zaskoczony problemem, po prostu zastanawiam się, jak sobie z tym radzicie?
Mógłbym oczywiście po prostu zwrócić ten sam widok zamiast przekierowania, ale mam logikę w metodzie „Utwórz”, która zapełnia dane widoku, które musiałbym powielić.
Jakieś sugestie?
Create
metodzie, która wypełnia ViewData i wywołaj ją wCreate
metodzie GET, a także w nieudanej gałęzi walidacji wCreate
metodzie POST.Create
, po prostu umieściłem to w jakiejś metodziepopulateStuff
, którą wywołuję zarówno w poleceniu, jakGET
i niepowodzeniuPOST
.Odpowiedzi:
Musisz mieć to samo wystąpienie
Review
na swojejHttpGet
akcji. Aby to zrobić, należy zapisać obiektReview review
w zmiennej tymczasowej wHttpPost
akcji, a następnie przywrócić go wHttpGet
akcji.[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"]}); } }
Jeśli chcesz, aby to działało, nawet jeśli przeglądarka jest odświeżana po pierwszym wykonaniu
HttpGet
akcji, możesz to zrobić:Review review = TempData["Review"] as Review; TempData["Review"] = review;
W przeciwnym razie obiekt przycisku odświeżania
review
będzie pusty, ponieważ nie będzie w nim żadnych danychTempData["Review"]
.źródło
TempData["ModelState"] = ModelState;
i przywróci zModelState.Merge((ModelStateDictionary)TempData["ModelState"]);
, to zadziałareturn Create(uniqueUri)
wtedy, gdy weryfikacja nie powiedzie się w POST? Ponieważ wartości ModelState mają pierwszeństwo przed ViewModel przekazanym do widoku, opublikowane dane powinny nadal pozostać.Musiałem dzisiaj sam rozwiązać ten problem i natknąłem się na to pytanie.
Niektóre odpowiedzi są przydatne (przy użyciu TempData), ale tak naprawdę nie odpowiadają na zadane pytanie.
Najlepsza rada, jaką znalazłem, znajduje się w tym wpisie na blogu:
http://www.jefclaes.be/2012/06/persisting-model-state-when-using-prg.html
Zasadniczo użyj TempData, aby zapisać i przywrócić obiekt ModelState. Jednak jest to o wiele czystsze, jeśli przeniesiesz to na atrybuty.
Na przykład
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"]); } } }
Następnie, zgodnie z przykładem, możesz zapisać / przywrócić ModelState w następujący sposób:
[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"]}); } }
Jeśli chcesz również przekazać model w TempData (jak zasugerował bigb), nadal możesz to zrobić.
źródło
NextRequest
iTempData
zachowuje się, gdy istnieje wiele kart przeglądarki wykonujących (wiele / jednoczesnych) żądań?Dlaczego nie utworzyć funkcji prywatnej z logiką w metodzie „Create” i wywołać tę metodę zarówno z metody Get, jak i Post, i po prostu zwrócić View ().
źródło
return Create(new { uniqueUri = ... });
. Twoja logika pozostaje SUCHA (podobnie jak wywołanieRedirectToAction
), ale bez problemów przenoszonych przez przekierowanie, takich jak utrata swojego ModelStatemógłbym użyć
TempData["Errors"]
TempData są przekazywane przez akcje zachowujące dane 1 raz.
źródło
Proponuję zwrócić widok i unikać powielania za pomocą atrybutu w akcji. Oto przykład wypełniania w celu wyświetlenia danych. Możesz zrobić coś podobnego z logiką metody tworzenia.
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 }
Oto przykład:
[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); }
źródło
Mam metodę, która dodaje stan modelu do danych tymczasowych. Mam wtedy metodę w moim kontrolerze podstawowym, która sprawdza dane tymczasowe pod kątem błędów. Jeśli je ma, dodaje je z powrotem do ModelState.
źródło
Mój scenariusz jest trochę bardziej skomplikowany, ponieważ używam wzorca PRG, więc mój ViewModel („SummaryVM”) jest w TempData, a mój ekran Podsumowanie wyświetla go. Na tej stronie znajduje się mały formularz, w którym można WYSŁAĆ pewne informacje do innej akcji. Powikłanie wynika z konieczności edytowania przez użytkownika niektórych pól w SummaryVM na tej stronie.
Summary.cshtml zawiera podsumowanie walidacji, które będzie wychwytywać błędy ModelState, które utworzymy.
@Html.ValidationSummary()
Mój formularz musi teraz POST do akcji HttpPost dla Summary (). Mam inny bardzo mały ViewModel do reprezentowania edytowanych pól, a powiązanie modelu dostarczy je do mnie.
Nowa forma:
@using (Html.BeginForm("Summary", "MyController", FormMethod.Post)) { @Html.Hidden("TelNo") @* // Javascript to update this *@
i akcja ...
[HttpPost] public ActionResult Summary(EditedItemsVM vm)
Tutaj przeprowadzam weryfikację i wykrywam złe dane wejściowe, więc muszę wrócić do strony Podsumowanie z błędami. Do tego używam TempData, który przetrwa przekierowanie. Jeśli nie ma problemu z danymi, zamieniam obiekt SummaryVM na kopię (ale oczywiście ze zmienionymi edytowanymi polami), a następnie wykonuję 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");
Akcja kontrolera podsumowania, od której wszystko się zaczyna, wyszukuje wszelkie błędy w tempdata i dodaje je do stanu modelu.
[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); } }
źródło
Firma Microsoft usunęła możliwość przechowywania złożonych typów danych w TempData, dlatego poprzednie odpowiedzi już nie działają; możesz przechowywać tylko proste typy, takie jak ciągi. Zmieniłem odpowiedź przez @ asgeo1, aby działała zgodnie z oczekiwaniami.
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); } } } }
W tym miejscu możesz po prostu dodać wymaganą adnotację danych do metody kontrolera, zgodnie z potrzebą.
[RestoreModelStateFromTempDataAttribute] [HttpGet] public async Task<IActionResult> MethodName() { } [SetTempDataModelStateAttribute] [HttpPost] public async Task<IActionResult> MethodName() { ModelState.AddModelError("KEY HERE", "ERROR HERE"); }
źródło
Wolę dodać metodę do mojego ViewModel, która wypełnia wartości domyślne:
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... } }
Wtedy nazywam to, gdy potrzebuję oryginalnych danych w ten sposób:
[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!"); }
źródło
Podaję tutaj przykładowy kod W swoim viewModel możesz dodać jedną właściwość typu "ModelStateDictionary" jako
public ModelStateDictionary ModelStateErrors { get; set; }
a w metodzie działania POST możesz pisać kod bezpośrednio jak
model.ModelStateErrors = ModelState;
a następnie przypisz ten model do Tempdata, jak poniżej
TempData["Model"] = model;
a kiedy przekierowujesz do metody akcji innego kontrolera, w kontrolerze musisz odczytać wartość 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); } }
Otóż to. Nie musisz w tym celu pisać filtrów akcji. Jest to tak proste, jak powyższy kod, jeśli chcesz przenieść błędy stanu modelu do innego widoku innego kontrolera.
źródło