Asp.net MVC ModelState.Clear

116

Adakah yang bisa memberi saya definisi singkat tentang peran ModelState di Asp.net MVC (atau tautan ke salah satunya). Secara khusus saya perlu tahu dalam situasi apa yang perlu atau diinginkan untuk menelepon ModelState.Clear().

Agak terbuka ya ... maaf, saya pikir mungkin membantu jika memberi tahu Anda apa yang sebenarnya saya lakukan:

Saya memiliki Tindakan Edit pada Pengontrol yang disebut "Halaman". Ketika saya pertama kali melihat formulir untuk mengubah detail Halaman semuanya dimuat dengan baik (mengikat ke objek "MyCmsPage"). Lalu saya klik tombol yang menghasilkan nilai untuk salah satu bidang objek MyCmsPage ( MyCmsPage.SeoTitle). Ini menghasilkan fine dan memperbarui objek dan saya kemudian mengembalikan hasil tindakan dengan objek halaman yang baru dimodifikasi dan mengharapkan kotak teks yang relevan (diberikan menggunakan <%= Html.TextBox("seoTitle", page.SeoTitle)%>) untuk diperbarui ... tetapi sayangnya ini menampilkan nilai dari model lama yang dimuat.

Saya telah mengatasinya dengan menggunakan ModelState.Clear()tetapi saya perlu tahu mengapa / bagaimana cara kerjanya jadi saya tidak hanya melakukannya secara membabi buta.

PageController:

[AcceptVerbs("POST")]
public ActionResult Edit(MyCmsPage page, string submitButton)
{
    // add the seoTitle to the current page object
    page.GenerateSeoTitle();

    // why must I do this?
    ModelState.Clear();

    // return the modified page object
     return View(page);
 }

Aspx:

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MyCmsPage>" %>
....
        <div class="c">
            <label for="seoTitle">
                Seo Title</label>
            <%= Html.TextBox("seoTitle", page.SeoTitle)%>
            <input type="submit" value="Generate Seo Title" name="submitButton" />
        </div>
Tuan Grok
sumber
Noob AspMVC, jika ingin menyimpan data lama, lalu apa gunanya memberikan model kepada pengguna lagi: @ saya punya masalah yang sama, terima kasih banyak bro
deadManN

Jawaban:

135

Saya pikir adalah bug di MVC. Saya bergumul dengan masalah ini selama berjam-jam hari ini.

Diberikan ini:

public ViewResult SomeAction(SomeModel model) 
{
    model.SomeString = "some value";
    return View(model); 
}

Tampilan dirender dengan model asli, mengabaikan perubahan. Jadi saya pikir, mungkin tidak suka saya menggunakan model yang sama, jadi saya coba seperti ini:

public ViewResult SomeAction(SomeModel model) 
{
    var newModel = new SomeModel { SomeString = "some value" };
    return View(newModel); 
}

Dan masih tampilan render dengan model aslinya. Yang aneh adalah, ketika saya meletakkan breakpoint dalam tampilan dan memeriksa modelnya, nilainya berubah. Tetapi aliran respons memiliki nilai-nilai lama.

Akhirnya saya menemukan pekerjaan yang sama yang Anda lakukan:

public ViewResult SomeAction(SomeModel model) 
{
    var newModel = new SomeModel { SomeString = "some value" };
    ModelState.Clear();
    return View(newModel); 
}

Bekerja seperti yang diharapkan.

Saya tidak berpikir ini adalah "fitur", bukan?

Tim Scott
sumber
33
Hanya melakukan hal yang hampir sama denganmu. Tapi ternyata ini bukan bug. Ini dengan desain: A Bug? EditorFor dan DisplayFor tidak menampilkan nilai yang sama dan Pembantu Html ASP.NET MVC
Metro Smurf
8
Sobat, aku sudah menghabiskan 2 jam berkelahi dengannya. Terima kasih telah memposting jawaban ini!
Andrey Agibalov
37
ini masih benar dan banyak orang, termasuk saya, kehilangan banyak waktu karena ini. bug atau desain, saya tidak peduli, itu "tidak terduga".
Proviste
7
Saya setuju dengan @Proviste, saya harap "fitur" ini dihapus di masa mendatang
Ben
8
Saya hanya menghabiskan empat jam untuk ini. Jelek.
Brian MacKay
46

Memperbarui:

  • Ini bukan bug.
  • Harap berhenti kembali View()dari tindakan POST. Gunakan PRG sebagai gantinya dan alihkan ke GET jika aksinya berhasil.
  • Jika Anda sedang mengembalikan View()dari tindakan POST, melakukannya untuk validasi form, dan melakukannya dengan cara MVC dirancang menggunakan dibangun di pembantu. Jika Anda melakukannya dengan cara ini maka Anda tidak perlu menggunakan.Clear()
  • Jika Anda menggunakan tindakan ini untuk mengembalikan ajax untuk SPA , gunakan pengontrol api web dan lupakan ModelStatekarena Anda tidak boleh menggunakannya.

Jawaban lama:

ModelState di MVC digunakan terutama untuk menggambarkan keadaan objek model sebagian besar berkaitan dengan apakah objek itu valid atau tidak. Tutorial ini harus menjelaskan banyak hal.

Umumnya Anda tidak perlu menghapus ModelState karena itu dikelola oleh mesin MVC untuk Anda. Menghapusnya secara manual dapat menyebabkan hasil yang tidak diinginkan saat mencoba mematuhi praktik terbaik validasi MVC.

Tampaknya Anda mencoba menyetel nilai default untuk judul. Ini harus dilakukan ketika objek model dibuat instance-nya (lapisan domain di suatu tempat atau di objek itu sendiri - ctor tanpa parameter), pada tindakan get sedemikian rupa sehingga ia turun ke halaman untuk pertama kalinya atau sepenuhnya pada klien (melalui ajax atau sesuatu) sehingga tampak seolah-olah pengguna memasukkannya dan kembali dengan koleksi formulir yang diposting. Beberapa cara pendekatan Anda menambahkan nilai ini pada penerimaan koleksi bentuk (dalam aksi POST // Ubah) yang menyebabkan perilaku aneh ini yang mungkin mengakibatkan .Clear() muncul untuk bekerja untuk Anda. Percayalah - Anda tidak ingin menggunakan clear. Cobalah salah satu ide lainnya.

Matt Kocaj
sumber
1
Apakah membantu saya untuk memikirkan kembali lapisan layanan saya sedikit (mengeluh tapi terima kasih) tetapi seperti banyak hal di internet itu sangat condong ke arah sudut pandang menggunakan ModelState untuk validasi.
Tuan Grok
Menambahkan lebih banyak informasi ke pertanyaan untuk menunjukkan mengapa saya sangat tertarik dengan ModelState.Clear () dan alasan pertanyaan saya
Mr Grok
5
Saya tidak benar-benar membeli argumen ini untuk berhenti mengembalikan View (...) dari fungsi [HttpPost]. Jika Anda melakukan POSTING konten melalui ajax dan kemudian memperbarui dokumen dengan PartialView yang dihasilkan, MVC ModelState telah terbukti salah. Satu-satunya solusi yang saya temukan adalah Menghapusnya dalam metode pengontrol.
Aaron Hudon
@AaronHudon PRG cukup mapan.
Matt Kocaj
Jika saya POST dengan panggilan AJAX, dapatkah saya mengalihkan ke tindakan GET dan mengembalikan tampilan yang diisi model seperti yang diinginkan OP, semuanya secara asinkron?
MyiEye
17

Jika Anda ingin menghapus nilai untuk bidang individu, saya menemukan teknik berikut berguna.

ModelState.SetModelValue("Key", new ValueProviderResult(null, string.Empty, CultureInfo.InvariantCulture));

Catatan: Ubah "Kunci" ke nama bidang yang ingin Anda setel ulang.

Carl Saunders
sumber
Saya tidak tahu mengapa ini bekerja secara berbeda untuk saya (mungkin MVC4)? Tapi saya juga harus melakukan model.Key = "" sesudahnya. Kedua baris tersebut harus ada.
TTT
Saya ingin memuji Anda atas komentar hapus @PeterGluck. Ini lebih baik daripada membersihkan status model lengkap (karena saya mendapat kesalahan pada beberapa bidang yang ingin saya pertahankan).
Tjab
6

Nah, ModelState pada dasarnya memegang Status model saat ini dalam hal validasi, yang dimilikinya

ModelErrorCollection: Mewakili kesalahan saat model mencoba mengikat nilai. ex.

TryUpdateModel();
UpdateModel();

atau seperti parameter di ActionResult

public ActionResult Create(Person person)

ValueProviderResult : Simpan detail tentang percobaan pengikatan ke model. ex. AttemptedValue, Culture, RawValue .

Metode Clear () harus digunakan dengan hati-hati karena dapat menyebabkan hasil yang tidak diharapkan. Dan Anda akan kehilangan beberapa properti bagus dari ModelState seperti AttemptedValue, ini digunakan oleh MVC di latar belakang untuk mengisi kembali nilai formulir jika terjadi kesalahan.

ModelState["a"].Value.AttemptedValue
JOBG
sumber
1
hmmm ... Mungkin di situlah saya mendapatkan masalah dari kelihatannya. Saya telah memeriksa nilai properti Model.SeoTitle dan itu telah berubah tetapi nilai yang dicoba belum. Tampak seolah-olah itu menempelkan nilai seolah-olah ada kesalahan pada halaman meskipun tidak ada (periksa Kamus ModelState dan tidak ada kesalahan).
Mr Grok
6

Saya memiliki contoh di mana saya ingin memperbarui model formulir penjumlahan, dan tidak ingin 'Redirect To Action' karena alasan performanace. Nilai sebelumnya dari bidang tersembunyi dipertahankan pada model saya yang diperbarui - menyebabkan semua jenis masalah !.

Beberapa baris kode segera mengidentifikasi elemen dalam ModelState yang ingin saya hapus (setelah validasi), jadi nilai baru digunakan dalam bentuk: -

while (ModelState.FirstOrDefault(ms => ms.Key.ToString().StartsWith("SearchResult")).Value != null)
{
    ModelState.Remove(ModelState.FirstOrDefault(ms => ms.Key.ToString().StartsWith("SearchResult")));
}
stevieg.dll
sumber
5

Banyak dari kita tampaknya telah digigit oleh ini, dan meskipun alasan ini terjadi masuk akal, saya membutuhkan cara untuk memastikan bahwa nilai pada Model saya ditampilkan, dan bukan ModelState.

Beberapa telah menyarankan ModelState.Remove(string key), tetapi tidak jelas apa yang keyseharusnya, terutama untuk model bersarang. Berikut adalah beberapa metode yang saya temukan untuk membantu hal ini.

The RemoveStateForMetode akan mengambil ModelStateDictionary, Model, dan ekspresi untuk properti yang diinginkan, dan menghapusnya. HiddenForModeldapat digunakan dalam Tampilan Anda untuk membuat bidang input tersembunyi hanya menggunakan nilai dari Model, dengan terlebih dahulu menghapus entri ModelState-nya. (Ini dapat dengan mudah diperluas untuk metode ekstensi helper lainnya).

/// <summary>
/// Returns a hidden input field for the specified property. The corresponding value will first be removed from
/// the ModelState to ensure that the current Model value is shown.
/// </summary>
public static MvcHtmlString HiddenForModel<TModel, TProperty>(this HtmlHelper<TModel> helper,
    Expression<Func<TModel, TProperty>> expression)
{
    RemoveStateFor(helper.ViewData.ModelState, helper.ViewData.Model, expression);
    return helper.HiddenFor(expression);
}

/// <summary>
/// Removes the ModelState entry corresponding to the specified property on the model. Call this when changing
/// Model values on the server after a postback, to prevent ModelState entries from taking precedence.
/// </summary>
public static void RemoveStateFor<TModel, TProperty>(this ModelStateDictionary modelState, TModel model,
    Expression<Func<TModel, TProperty>> expression)
{
    var key = ExpressionHelper.GetExpressionText(expression);

    modelState.Remove(key);
}

Panggil dari pengontrol seperti ini:

ModelState.RemoveStateFor(model, m => m.MySubProperty.MySubValue);

atau dari tampilan seperti ini:

@Html.HiddenForModel(m => m.MySubProperty.MySubValue)

Ini digunakan System.Web.Mvc.ExpressionHelperuntuk mendapatkan nama properti ModelState.

Tobias J
sumber
1
Sangat bagus! Menjaga tab ini untuk fungsionalitas ExpressionHelper.
Gerard ONeill
4

Saya ingin memperbarui atau mengatur ulang nilai jika tidak cukup memvalidasi, dan mengalami masalah ini.

Jawaban mudahnya, ModelState.Remove, adalah .. bermasalah .. karena jika Anda menggunakan pembantu Anda tidak benar-benar tahu namanya (kecuali Anda tetap berpegang pada konvensi penamaan). Kecuali mungkin Anda membuat fungsi yang baik Anda kustom dapat digunakan oleh pembantu dan pengontrol Anda untuk mendapatkan nama.

Fitur ini seharusnya diimplementasikan sebagai opsi pada helper, di mana secara default tidak melakukan ini, tetapi jika Anda ingin input yang tidak diterima untuk ditampilkan kembali Anda bisa mengatakannya.

Tapi setidaknya saya mengerti masalahnya sekarang;).

Gerard ONeill
sumber
Saya perlu melakukan ini; lihat metode saya yang saya posting di bawah ini yang membantu saya Remove()kunci yang benar.
Tobias J
0

Mengerti pada akhirnya. ModelBinder Kustom saya yang tidak terdaftar dan melakukan ini:

var mymsPage = new MyCmsPage();

NameValueCollection frm = controllerContext.HttpContext.Request.Form;

myCmsPage.SeoTitle = (!String.IsNullOrEmpty(frm["seoTitle"])) ? frm["seoTitle"] : null;

Jadi sesuatu yang dilakukan oleh pengikatan model default pasti menyebabkan masalah. Tidak yakin apa, tetapi masalah saya setidaknya sudah diperbaiki sekarang karena pengikat model khusus saya sedang terdaftar.

Tuan Grok
sumber
Yah saya tidak punya pengalaman dengan ModelBinder kustom, yang default sesuai dengan kebutuhan saya sejauh ini =).
JOBG
0

Secara umum, ketika Anda menemukan diri Anda berjuang melawan praktik standar kerangka kerja, inilah saatnya untuk mempertimbangkan kembali pendekatan Anda. Dalam hal ini, perilaku ModelState. Misalnya, ketika Anda tidak menginginkan status model setelah POST, pertimbangkan pengalihan ke get.

[HttpPost]
public ActionResult Edit(MyCmsPage page, string submitButton)
{
    if (ModelState.IsValid) {
        SomeRepository.SaveChanges(page);
        return RedirectToAction("GenerateSeoTitle",new { page.Id });
    }
    return View(page);
}

public ActionResult GenerateSeoTitle(int id) {
     var page = SomeRepository.Find(id);
     page.GenerateSeoTitle();
     return View("Edit",page);
}

DIEDIT untuk menjawab komentar budaya:

Inilah yang saya gunakan untuk menangani aplikasi MVC multi-budaya. Pertama subkelas pengendali rute:

public class SingleCultureMvcRouteHandler : MvcRouteHandler {
    protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        var culture = requestContext.RouteData.Values["culture"].ToString();
        if (string.IsNullOrWhiteSpace(culture))
        {
            culture = "en";
        }
        var ci = new CultureInfo(culture);
        Thread.CurrentThread.CurrentUICulture = ci;
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(ci.Name);
        return base.GetHttpHandler(requestContext);
    }
}

public class MultiCultureMvcRouteHandler : MvcRouteHandler
{
    protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        var culture = requestContext.RouteData.Values["culture"].ToString();
        if (string.IsNullOrWhiteSpace(culture))
        {
            culture = "en";
        }
        var ci = new CultureInfo(culture);
        Thread.CurrentThread.CurrentUICulture = ci;
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(ci.Name);
        return base.GetHttpHandler(requestContext);
    }
}

public class CultureConstraint : IRouteConstraint
{
    private string[] _values;
    public CultureConstraint(params string[] values)
    {
        this._values = values;
    }

    public bool Match(HttpContextBase httpContext,Route route,string parameterName,
                        RouteValueDictionary values, RouteDirection routeDirection)
    {

        // Get the value called "parameterName" from the 
        // RouteValueDictionary called "value"
        string value = values[parameterName].ToString();
        // Return true is the list of allowed values contains 
        // this value.
        return _values.Contains(value);

    }

}

public enum Culture
{
    es = 2,
    en = 1
}

Dan inilah cara saya mengatur rute. Setelah membuat rute, saya menambahkan subagen saya (example.com/subagent1, example.com/subagent2, dll) lalu kode budaya. Jika yang Anda butuhkan hanyalah budaya, cukup hapus subagen dari penangan rute dan rute.

    public static void RegisterRoutes(RouteCollection routes)
    {

        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
        routes.IgnoreRoute("Content/{*pathInfo}");
        routes.IgnoreRoute("Cache/{*pathInfo}");
        routes.IgnoreRoute("Scripts/{pathInfo}.js");
        routes.IgnoreRoute("favicon.ico");
        routes.IgnoreRoute("apple-touch-icon.png");
        routes.IgnoreRoute("apple-touch-icon-precomposed.png");

        /* Dynamically generated robots.txt */
        routes.MapRoute(
            "Robots.txt", "robots.txt",
            new { controller = "Robots", action = "Index", id = UrlParameter.Optional }
        );

        routes.MapRoute(
             "Sitemap", // Route name
             "{subagent}/sitemap.xml", // URL with parameters
             new { subagent = "aq", controller = "Default", action = "Sitemap"},  new[] { "aq3.Controllers" } // Parameter defaults
        );

        routes.MapRoute(
             "Rss Feed", // Route name
             "{subagent}/rss", // URL with parameters
             new { subagent = "aq", controller = "Default", action = "RSS"},  new[] { "aq3.Controllers" } // Parameter defaults
        );

        /* remap wordpress tags to mvc blog posts */
        routes.MapRoute(
            "Tag", "tag/{title}",
            new { subagent = "aq", controller = "Default", action = "ThreeOhOne", id = UrlParameter.Optional},  new[] { "aq3.Controllers" }
        ).RouteHandler = new MultiCultureMvcRouteHandler(); ;

        routes.MapRoute(
            "Custom Errors", "Error/{*errorType}",
            new { controller = "Error", action = "Index", id = UrlParameter.Optional},  new[] { "aq3.Controllers" }
        );

        /* dynamic images not loaded from content folder */
        routes.MapRoute(
            "Stock Images",
            "{subagent}/Images/{*filename}",
            new { subagent = "aq", controller = "Image", action = "Show", id = UrlParameter.Optional, culture = "en"},  new[] { "aq3.Controllers" }
        );

        /* localized routes follow */
        routes.MapRoute(
            "Localized Images",
            "Images/{*filename}",
            new { subagent = "aq", controller = "Image", action = "Show", id = UrlParameter.Optional},  new[] { "aq3.Controllers" }
        ).RouteHandler = new MultiCultureMvcRouteHandler();

        routes.MapRoute(
            "Blog Posts",
            "Blog/{*postname}",
            new { subagent = "aq", controller = "Blog", action = "Index", id = UrlParameter.Optional},  new[] { "aq3.Controllers" }
        ).RouteHandler = new MultiCultureMvcRouteHandler();

        routes.MapRoute(
            "Office Posts",
            "Office/{*address}",
            new { subagent = "aq", controller = "Offices", action = "Address", id = UrlParameter.Optional }, new[] { "aq3.Controllers" }
        ).RouteHandler = new MultiCultureMvcRouteHandler();

        routes.MapRoute(
             "Default", // Route name
             "{controller}/{action}/{id}", // URL with parameters
             new { subagent = "aq", controller = "Home", action = "Index", id = UrlParameter.Optional }, new[] { "aq3.Controllers" } // Parameter defaults
        ).RouteHandler = new MultiCultureMvcRouteHandler();

        foreach (System.Web.Routing.Route r in routes)
        {
            if (r.RouteHandler is MultiCultureMvcRouteHandler)
            {
                r.Url = "{subagent}/{culture}/" + r.Url;
                //Adding default culture 
                if (r.Defaults == null)
                {
                    r.Defaults = new RouteValueDictionary();
                }
                r.Defaults.Add("culture", Culture.en.ToString());

                //Adding constraint for culture param
                if (r.Constraints == null)
                {
                    r.Constraints = new RouteValueDictionary();
                }
                r.Constraints.Add("culture", new CultureConstraint(Culture.en.ToString(), Culture.es.ToString()));
            }
        }

    }
B2K
sumber
Anda sangat tepat menyarankan praktik POST REDIRECT, sebenarnya saya melakukan ini untuk hampir setiap tindakan posting. Namun saya memiliki kebutuhan yang sangat khusus: saya memiliki formulir filter di bagian atas halaman, awalnya dikirimkan dengan get. Tetapi saya mengalami masalah dengan bidang tanggal yang tidak terikat dan kemudian menemukan bahwa permintaan GET tidak membawa budaya sekitar (saya menggunakan bahasa Prancis untuk aplikasi saya), jadi saya harus mengalihkan permintaan ke POST agar berhasil mengikat tanggal saya. Lalu datang masalah ini, saya sedikit terjebak dia ..
Souhaieb Besbes
@SouhaiebBesbes Lihat pembaruan saya yang menunjukkan bagaimana saya menangani budaya.
B2K
@SouhaiebBesbes mungkin sedikit lebih sederhana untuk menyimpan budaya Anda di TempData. Lihat stackoverflow.com/questions/12422930/…
B2K
0

Nah, ini sepertinya berfungsi di Halaman Razor saya dan bahkan tidak pernah melakukan perjalanan pulang pergi ke file .cs. Ini adalah cara html lama. Mungkin berguna.

<input type="reset" value="Reset">
JustJohn
sumber