ASP.NET MVC - Bagaimana Mempertahankan Kesalahan ModelState di RedirectToAction?

92

Saya memiliki dua metode tindakan berikut (disederhanakan untuk pertanyaan):

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

Jadi, jika validasi lolos, saya redirect ke halaman lain (konfirmasi).

Jika terjadi kesalahan, saya perlu menampilkan halaman yang sama dengan kesalahan tersebut.

Jika saya lakukan return View(), kesalahan ditampilkan, tetapi jika saya lakukan return RedirectToAction(seperti di atas), itu kehilangan kesalahan Model.

Saya tidak terkejut dengan masalah ini, hanya bertanya-tanya bagaimana kalian menangani ini?

Tentu saja saya bisa mengembalikan Tampilan yang sama daripada pengalihan, tetapi saya memiliki logika dalam metode "Buat" yang mengisi data tampilan, yang harus saya duplikat.

Ada saran?

RPM 1984
sumber
10
Saya mengatasi masalah ini dengan tidak menggunakan pola Post-Redirect-Get untuk kesalahan validasi. Saya hanya menggunakan View (). Sangat valid untuk melakukan itu alih-alih melewati banyak rintangan - dan mengalihkan kekacauan dengan riwayat browser Anda.
Jimmy Bogard
2
Dan selain yang dikatakan @JimmyBogard, ekstrak logika dalam Createmetode yang mengisi ViewData dan panggil dalam Createmetode GET dan juga di cabang validasi yang gagal dalam Createmetode POST.
Russ Cam
1
Setuju, menghindari masalah adalah salah satu cara untuk mengatasinya. Saya memiliki beberapa logika untuk mengisi hal-hal dalam Createpandangan saya , saya hanya memasukkannya ke dalam beberapa metode populateStuffyang saya panggil baik dalam GETdan gagal POST.
Francois Joly
12
@JimmyBogard Saya tidak setuju, jika Anda memposting ke suatu tindakan dan kemudian mengembalikan tampilan Anda mengalami masalah di mana jika pengguna menekan refresh mereka mendapatkan peringatan tentang keinginan untuk memulai posting itu lagi.
The Muffin Man

Jawaban:

50

Anda harus memiliki contoh yang sama Reviewtentang HttpGettindakan Anda . Untuk melakukan itu, Anda harus menyimpan objek Review reviewdalam variabel temp pada HttpPosttindakan Anda dan kemudian mengembalikannya pada HttpGettindakan.

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

Jika Anda ingin ini berfungsi meskipun browser di-refresh setelah eksekusi pertama HttpGettindakan, Anda dapat melakukan ini:

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

Jika tidak, objek tombol segarkan reviewakan kosong karena tidak akan ada data masuk TempData["Review"].

kuncevic.dev
sumber
2
Luar biasa. Dan +1 besar untuk menyebutkan masalah penyegaran. Ini jawaban terlengkap jadi saya terima, terima kasih banyak. :)
RPM1984
8
Ini tidak benar-benar menjawab pertanyaan dalam judul. ModelState tidak dipertahankan dan yang memiliki konsekuensi seperti input HtmlHelpers tidak mempertahankan entri pengguna. Ini hampir merupakan solusi.
John Farrell
Saya akhirnya melakukan apa yang @Wim sarankan dalam jawabannya.
RPM1984
17
@jfar, saya setuju, jawaban ini tidak berfungsi dan tidak bertahan di ModelState. Namun, jika Anda memodifikasinya sehingga melakukan sesuatu seperti TempData["ModelState"] = ModelState; dan memulihkan ModelState.Merge((ModelStateDictionary)TempData["ModelState"]);, maka itu akan berfungsi
asgeo1
1
Tidak bisakah Anda hanya return Create(uniqueUri)ketika validasi gagal pada POST? Karena nilai ModelState lebih diutamakan daripada ViewModel yang diteruskan ke tampilan, data yang diposting harus tetap ada.
ajbeaven
84

Saya harus menyelesaikan masalah ini sendiri hari ini, dan menemukan pertanyaan ini.

Beberapa jawaban berguna (menggunakan TempData), tetapi tidak benar-benar menjawab pertanyaan yang ada.

Saran terbaik yang saya temukan ada di entri blog ini:

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

Pada dasarnya, gunakan TempData untuk menyimpan dan memulihkan objek ModelState. Namun, jauh lebih bersih jika Anda mengabstraksi ini menjadi atribut.

Misalnya

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

Kemudian sesuai contoh Anda, Anda dapat menyimpan / memulihkan ModelState seperti:

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

Jika Anda juga ingin meneruskan model di TempData (seperti yang disarankan bigb) maka Anda masih bisa melakukannya juga.

asgeo1
sumber
Terima kasih. Kami menerapkan sesuatu yang mirip dengan pendekatan Anda. gist.github.com/ferventcoder/4735084
ferventcoder
Jawaban yang bagus. Terima kasih.
Mark Vickery
3
Solusi ini adalah alasan saya menggunakan stackoverflow. Terima kasih sobat!
jugg1es
@ asgeo1 - solusi hebat, tetapi saya mengalami masalah saat menggunakannya dalam kombinasi dengan Tampilan Parsial yang berulang, saya memposting pertanyaan di sini: stackoverflow.com/questions/28372330/…
Josh
Contoh bagus untuk mengambil solusi sederhana dan membuatnya sangat elegan, dalam semangat MVC. Sangat bagus!
AHowgego
7

Mengapa tidak membuat fungsi privat dengan logika dalam metode "Buat" dan memanggil metode ini dari metode Get dan Post dan cukup mengembalikan View ().

Wim
sumber
Ini sebenarnya yang akhirnya saya lakukan - Anda membaca pikiran saya. +1 :)
RPM1984
1
Inilah yang saya lakukan juga, alih-alih memiliki fungsi pribadi, saya hanya meminta metode POST saya memanggil metode GET pada kesalahan (yaitu return Create(new { uniqueUri = ... });. Logika Anda tetap KERING (seperti memanggil RedirectToAction), tetapi tanpa masalah yang dibawa oleh pengalihan, seperti kehilangan ModelState Anda.
Daniel Liuzzi
1
@DanielLiuzzi: melakukannya dengan cara itu tidak akan mengubah URL. Jadi Anda mengakhiri dengan url sesuatu seperti "/ controller / create /".
Skorunka František
@ Skorunkarantišek Dan itulah intinya. Pertanyaannya menyatakan Jika terjadi kesalahan, saya perlu menampilkan halaman yang sama dengan kesalahan tersebut. Dalam konteks ini, sangat dapat diterima (dan lebih disukai IMO) bahwa URL TIDAK berubah jika halaman yang sama ditampilkan. Juga, satu keuntungan dari pendekatan ini adalah bahwa jika kesalahan yang dimaksud bukanlah kesalahan validasi tetapi kesalahan sistem (waktu tunggu DB misalnya) memungkinkan pengguna untuk menyegarkan halaman untuk mengirim kembali formulir.
Daniel Liuzzi
4

Saya bisa menggunakan TempData["Errors"]

TempData diteruskan melalui tindakan yang menyimpan data 1 kali.

merampok waminal
sumber
4

Saya sarankan Anda mengembalikan tampilan, dan menghindari duplikasi melalui atribut pada tindakan. Berikut adalah contoh pengisian untuk melihat data. Anda bisa melakukan hal serupa dengan logika metode create Anda.

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
}

Berikut ini contohnya:

[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);
}
CRice
sumber
Bagaimana ini ide yang buruk? Saya pikir atribut menghindari kebutuhan untuk menggunakan tindakan lain karena kedua tindakan dapat menggunakan atribut untuk memuat ke ViewData.
CRice
1
Silakan lihat pola Post / Redirect / Get: en.wikipedia.org/wiki/Post/Redirect/Get
DreamSonic
2
Itu biasanya digunakan setelah validasi model terpenuhi, untuk mencegah posting lebih lanjut ke formulir yang sama saat refresh. Tetapi jika formulir tersebut memiliki masalah, maka perlu diperbaiki dan diposkan ulang. Pertanyaan ini berkaitan dengan penanganan kesalahan model.
CRice
Filter adalah untuk kode yang dapat digunakan kembali pada tindakan, terutama berguna untuk meletakkan sesuatu di ViewData. TempData hanyalah solusi.
CRice
1
@ppumkin mungkin mencoba memposting dengan ajax sehingga Anda tidak mengalami kesulitan untuk membangun kembali sisi server tampilan Anda.
CRice
2

Saya memiliki metode yang menambahkan status model ke data temp. Saya kemudian memiliki metode di pengontrol dasar saya yang memeriksa data temp untuk kesalahan apa pun. Jika memilikinya, itu menambahkan mereka kembali ke ModelState.

nick
sumber
1

Skenario saya sedikit lebih rumit karena saya menggunakan pola PRG sehingga ViewModel saya ("SummaryVM") ada di TempData, dan layar Ringkasan saya menampilkannya. Ada formulir kecil di halaman ini untuk POST beberapa info ke Action lain. Komplikasi berasal dari persyaratan bagi pengguna untuk mengedit beberapa bidang di SummaryVM pada halaman ini.

Summary.cshtml memiliki ringkasan validasi yang akan menangkap kesalahan ModelState yang akan kita buat.

@Html.ValidationSummary()

Formulir saya sekarang perlu POST ke tindakan HttpPost untuk Summary (). Saya memiliki ViewModel lain yang sangat kecil untuk mewakili bidang yang diedit, dan modelbinding akan memberikannya kepada saya.

Bentuk baru:

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

dan aksinya ...

[HttpPost]
public ActionResult Summary(EditedItemsVM vm)

Di sini saya melakukan beberapa validasi dan mendeteksi beberapa input yang buruk, jadi saya perlu kembali ke halaman Ringkasan dengan kesalahan. Untuk ini saya menggunakan TempData, yang akan bertahan dari pengalihan. Jika tidak ada masalah dengan data, saya mengganti objek SummaryVM dengan salinan (tetapi tentu saja dengan bidang yang diedit berubah) kemudian lakukan 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");

Tindakan pengontrol Ringkasan, tempat semua ini dimulai, mencari error apa pun dalam tempdata dan menambahkannya ke 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);
            }
        }
VictorySaber
sumber
1

Microsoft menghapus kemampuan untuk menyimpan tipe data kompleks di TempData, oleh karena itu jawaban sebelumnya tidak lagi berfungsi; Anda hanya dapat menyimpan jenis sederhana seperti string. Saya telah mengubah jawaban oleh @ asgeo1 agar berfungsi seperti yang diharapkan.

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

Dari sini, Anda cukup menambahkan anotasi data yang diperlukan pada metode pengontrol sesuai kebutuhan.

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


[SetTempDataModelStateAttribute]
[HttpPost]
public async Task<IActionResult> MethodName()
{
    ModelState.AddModelError("KEY HERE", "ERROR HERE");
}
Alex Marchant
sumber
Bekerja dengan sempurna !. Mengedit jawaban untuk memperbaiki kesalahan braket kecil saat menempelkan kode.
VDWWD
0

Saya lebih suka menambahkan metode ke ViewModel saya yang mengisi nilai default:

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

Kemudian saya menyebutnya ketika saya membutuhkan data asli seperti ini:

    [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!");
    }
Mohammed Noureldin
sumber
0

Saya hanya memberikan kode contoh di sini Di viewModel Anda, Anda dapat menambahkan satu properti jenis "ModelStateDictionary" sebagai

public ModelStateDictionary ModelStateErrors { get; set; }

dan dalam metode tindakan POST, Anda dapat menulis kode secara langsung seperti

model.ModelStateErrors = ModelState; 

dan kemudian tetapkan model ini ke Tempdata seperti di bawah ini

TempData["Model"] = model;

dan ketika Anda mengarahkan ke metode tindakan pengontrol lain maka di pengontrol Anda harus membaca nilai 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);
    }
}

Itu dia. Anda tidak perlu menulis filter tindakan untuk ini. Ini sesederhana kode di atas jika Anda ingin mendapatkan kesalahan status Model ke tampilan lain dari pengontrol lain.

RohanGarud
sumber