ASP.NET MVC - RedirectToAction Boyunca ModelState Hataları Nasıl Korunur?


93

Aşağıdaki iki eylem yöntemine sahibim (soru için basitleştirilmiş):

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

Yani doğrulama başarılı olursa başka bir sayfaya yönlendiriyorum (onay).

Bir hata oluşursa, aynı sayfayı hatayla görüntülemem gerekir.

Yaparsam return View()hata görüntülenir, ancak yaparsam return RedirectToAction(yukarıdaki gibi) Model hatalarını kaybeder.

Bu soruna şaşırmadım, sadece bununla nasıl başa çıktığınızı merak ediyorum?

Elbette, yönlendirme yerine aynı Görünümü döndürebilirim, ancak "Oluştur" yönteminde, çoğaltmam gereken görünüm verilerini dolduran mantığım var.

Herhangi bir öneri?


10
Doğrulama hataları için Post-Redirect-Get kalıbını kullanmayarak bu sorunu çözüyorum. Sadece View () kullanıyorum. Bir grup çemberden atlamak ve tarayıcı geçmişinizle karışıklıkları yeniden yönlendirmek yerine bunu yapmak tamamen geçerlidir.
Jimmy Bogard

2
Ve @JimmyBogard'ın söylediklerine ek olarak, CreateViewData'yı dolduran yöntemdeki mantığı çıkarın ve CreateGET yönteminde ve ayrıca CreatePOST yöntemindeki başarısız doğrulama dalında çağırın .
Russ Cam

1
Kabul edildi, problemden kaçınmak onu çözmenin bir yoludur. Benim görüşüme göre bazı şeyleri doldurmak için bir mantığım var Create, sadece onu populateStuffhem içinde hem GETde başarısız olarak adlandırdığım bir yönteme koydum POST.
Francois Joly

12
@JimmyBogard Katılmıyorum, bir eyleme gönderip sonra sorunla karşılaştığınız görünüme geri dönerseniz, kullanıcı yenilemeyi tıklarsa o gönderiyi tekrar başlatmak isteme uyarısı alırlar.
The Muffin Man

Yanıtlar:


50

Sen aynı örneğini olması gerekir Reviewsenin üzerine HttpGetharekete. Bunu yapmak Review reviewiçin HttpPosteyleminizdeki temp değişkenindeki bir nesneyi kaydetmeli ve ardından eylemde geri yüklemelisiniz 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"]});
   }   
}

HttpGetEylemin ilk çalıştırılmasından sonra tarayıcı yenilense bile bunun çalışmasını istiyorsanız, bunu yapabilirsiniz:

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

Aksi takdirde, yenileme düğmesi nesnesi reviewboş olacaktır çünkü içinde herhangi bir veri olmayacaktır TempData["Review"].


2
Mükemmel. Ve yenileme sorunundan bahsetmek için büyük bir +1. Bu en eksiksiz cevap, bu yüzden kabul edeceğim, çok teşekkürler. :)
RPM1984

8
Bu, başlıktaki soruya gerçekten cevap vermiyor. ModelState korunmaz ve bu, HtmlHelpers'ın kullanıcı girişini korumaması gibi sonuçları vardır. Bu neredeyse bir çözümdür.
John Farrell

@ Wim'in cevabında önerdiği şeyi yaptım.
RPM1984

17
@jfar, kabul ediyorum, bu cevap işe yaramıyor ve ModelState için ısrar etmiyor. Ancak, onu değiştirirseniz, benzer bir şey yapar TempData["ModelState"] = ModelState; ve geri ModelState.Merge((ModelStateDictionary)TempData["ModelState"]);
yüklerseniz

1
Sadece return Create(uniqueUri)POST'ta doğrulama başarısız olduğunda olamaz mı? ModelState değerleri, görünüme aktarılan ViewModel'e göre öncelik kazandığından, yayınlanan veriler yine de kalmalıdır.
ajbeaven

84

Bu sorunu bugün kendim çözmek zorunda kaldım ve bu soruyla karşılaştım.

Cevaplardan bazıları yararlıdır (TempData kullanılarak), ancak eldeki soruyu gerçekten yanıtlamaz.

Bulduğum en iyi tavsiye bu blog gönderisiydi:

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

Temel olarak, ModelState nesnesini kaydetmek ve geri yüklemek için TempData'yı kullanın. Ancak, bunu özniteliklere ayırırsanız çok daha temiz olur.

Örneğin

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

Ardından, örneğinize göre, ModelState'i şu şekilde kaydedebilir / geri yükleyebilirsiniz:

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

Modeli (bigb'nin önerdiği gibi) TempData'da da iletmek istiyorsanız, bunu da yapabilirsiniz.


Teşekkür ederim. Yaklaşımınıza benzer bir şey uyguladık. gist.github.com/ferventcoder/4735084
ferventcoder

@ asgeo1 - harika bir çözüm, ancak onu tekrar eden Kısmi Görünümlerle birlikte kullanırken bir sorunla karşılaştım, soruyu buraya gönderdim: stackoverflow.com/questions/28372330/…
Josh

Uyarı - sayfaya hepsi bir arada istekte sunulursa (ve AJAX aracılığıyla bölünmemişse), TempData bir sonraki isteğe kadar korunduğundan, bu çözümü kullanırken sorun yaşarsınız . Örneğin: bir sayfaya arama kriterleri girersiniz, ardından arama sonuçları için PRG girersiniz, ardından doğrudan arama sayfasına geri dönmek için bir bağlantıya tıklarsınız, orijinal arama değerleri yeniden doldurulur. Diğer tuhaf ve bazen yeniden üretilmesi zor davranışlar da ortaya çıkıyor.
PJ7

Oturum kimliğinin değişmeye devam ettiğini fark edene kadar bu işi yapamadım. Bu, sorunu
çözmeme

S: (çoklu / eşzamanlı) istek yapan birden çok tarayıcı sekmesi olduğunda ne olur NextRequestve ne olur TempData?
dan

7

Neden "Create" yöntemindeki mantıkla ve bu yöntemi hem Get hem de Post yönteminden çağırıp View () döndürerek özel bir işlev oluşturmuyorsunuz?


1
Ben de bunu yapıyorum, sadece özel bir işleve sahip olmak yerine, POST yöntemimin hata durumunda GET yöntemini çağırmasını sağlıyorum (yani, return Create(new { uniqueUri = ... });mantığınız KURU kalıyor (arama gibi RedirectToAction), ancak yeniden yönlendirmeyle taşınan sorunlar olmadan, örneğin ModelState'inizi kaybetmek.
Daniel Liuzzi

1
@DanielLiuzzi: Bunu bu şekilde yapmak URL'yi değiştirmez. Böylece url ile "/ controller / create /" gibi bir şey bitirirsiniz.
Skorunka František

@ SkorunkaFrantišek İşte asıl mesele bu. Soru , bir hata oluşursa, hatanın bulunduğu aynı sayfayı görüntülemem gerektiğini belirtir . Bu bağlamda, aynı sayfa görüntülendiğinde URL'nin DEĞİŞMEMESİ tamamen kabul edilebilir (ve tercih edilen IMO). Ayrıca, bu yaklaşımın sahip olduğu bir avantaj, söz konusu hata bir doğrulama hatası değilse de bir sistem hatasıysa (örneğin, DB zaman aşımı), kullanıcının formu yeniden göndermek için sayfayı yenilemesine izin vermesidir.
Daniel Liuzzi

4

kullanabilirim TempData["Errors"]

TempData, verileri 1 kez koruyarak eylemler arasında iletilir.


4

Görünümü geri döndürmenizi ve eylemdeki bir öznitelik yoluyla yinelemekten kaçınmanızı öneririm. Verileri görüntülemek için doldurmanın bir örneğini burada bulabilirsiniz. Oluşturma yöntemi mantığınızla benzer bir şey yapabilirsiniz.

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
}

İşte bir örnek:

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

Bu nasıl kötü bir fikir? Bence öznitelik başka bir işlem kullanma ihtiyacını ortadan kaldırıyor çünkü her iki işlem de ViewData'ya yüklemek için özniteliği kullanabilir.
CRice

1
Lütfen Post / Redirect / Get
pattern'e

2
Bu, normalde model doğrulaması karşılandıktan sonra, yenilemede aynı forma daha fazla gönderi yapılmasını önlemek için kullanılır. Ancak formda sorunlar varsa, yine de düzeltilmesi ve yeniden yayınlanması gerekir. Bu soru model hatalarının ele alınmasıyla ilgilidir.
CRice

Filtreler, eylemlerde yeniden kullanılabilir kod içindir, özellikle bir şeyleri ViewData'ya koymak için kullanışlıdır. TempData yalnızca bir çözümdür.
CRice

1
@ppumkin belki ajax ile gönderi paylaşmayı deneyebilir, böylece sunucu tarafınızı yeniden inşa etmekte zorlanmayabilirsiniz.
CRice

2

Model durumunu geçici verilere ekleyen bir yöntemim var. Daha sonra temel denetleyicimde herhangi bir hata için geçici verileri kontrol eden bir yöntem var. Onlara sahipse, onları ModelState'e geri ekler.


2

Microsoft, karmaşık veri türlerini TempData'da saklama yeteneğini kaldırdı, bu nedenle önceki yanıtlar artık çalışmıyor; dizeler gibi yalnızca basit türleri saklayabilirsiniz. Beklendiği gibi çalışması için cevabı @ asgeo1 ile değiştirdim.

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

Buradan, gerekli veri açıklamasını gerektiği gibi bir kontrolör yöntemine ekleyebilirsiniz.

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


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

Mükemmel çalışıyor!. Kodu yapıştırırken küçük bir parantez hatasını düzeltmek için yanıtı düzenledi.
VDWWD

.Net çekirdek 2.1'de çalışan tek cevap budur.
Robbie Chiha

1

PRG modelini kullandığım için senaryom biraz daha karmaşık, bu yüzden ViewModel'im ("SummaryVM") TempData'da ve Özet ekranım bunu gösteriyor. Bu sayfada başka bir Eyleme bazı bilgileri YAYINLAMAK için küçük bir form var. Karmaşıklık, kullanıcının bu sayfadaki SummaryVM'deki bazı alanları düzenleme gereksiniminden kaynaklanmıştır.

Summary.cshtml, oluşturacağımız ModelState hatalarını yakalayacak doğrulama özetine sahiptir.

@Html.ValidationSummary()

Formumun artık Summary () için bir HttpPost eylemine POST yapması gerekiyor. Düzenlenmiş alanları temsil etmek için çok küçük bir ViewModel'im daha var ve model ciltleme bunları bana ulaştıracak.

Yeni biçim:

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

ve eylem ...

[HttpPost]
public ActionResult Summary(EditedItemsVM vm)

Burada bazı doğrulamalar yapıyorum ve bazı hatalı girişler tespit ediyorum, bu nedenle hataları içeren Özet sayfasına dönmem gerekiyor. Bunun için yeniden yönlendirmeden kurtulacak TempData kullanıyorum. Verilerle ilgili bir sorun yoksa, SummaryVM nesnesini bir kopya ile değiştiririm (ancak düzenlenmiş alanlar elbette değiştirilir) sonra bir RedirectToAction ("NextAction") yaparım;

// 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");

Tüm bunların başladığı Özet denetleyici eylemi, tempdata'daki hataları arar ve bunları model durumuna ekler.

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

0

ViewModel'ime varsayılan değerleri dolduran bir yöntem eklemeyi tercih ediyorum:

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

Daha sonra, orijinal verilere şu şekilde ihtiyaç duyduğumda diyorum:

    [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

Burada sadece örnek kod veriyorum viewModel'inizde "ModelStateDictionary" türünde bir özellik ekleyebilirsiniz.

public ModelStateDictionary ModelStateErrors { get; set; }

ve POST eylemi yönteminizde doğrudan şu şekilde kod yazabilirsiniz:

model.ModelStateErrors = ModelState; 

ve sonra bu modeli aşağıdaki gibi Tempdata'ya atayın

TempData["Model"] = model;

ve diğer denetleyicinin eylem yöntemine yeniden yönlendirdiğinizde, denetleyicide Tempdata değerini okumanız gerekir

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

Bu kadar. Bunun için eylem filtreleri yazmanıza gerek yok. Model durumu hatalarını başka bir denetleyicinin başka bir görünümüne almak istiyorsanız, bu yukarıdaki kod kadar basittir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.