Asp.net MVC ModelState.Clear


116

Bất cứ ai có thể cho tôi một định nghĩa ngắn gọn về vai trò của ModelState trong Asp.net MVC (hoặc một liên kết đến một). Đặc biệt tôi cần biết trong những trường hợp nào cần thiết hoặc mong muốn gọi điện ModelState.Clear().

Bit open đã kết thúc huh ... xin lỗi, tôi nghĩ có thể hữu ích nếu cho bạn biết những gì tôi đang làm:

Tôi có một Hành động chỉnh sửa trên Bộ điều khiển được gọi là "Trang". Khi tôi lần đầu tiên nhìn thấy biểu mẫu để thay đổi thông tin chi tiết của Trang, mọi thứ đều tải tốt (liên kết với một đối tượng "MyCmsPage"). Sau đó, tôi nhấp vào nút tạo giá trị cho một trong các trường của đối tượng MyCmsPage ( MyCmsPage.SeoTitle). Nó tạo tốt và cập nhật đối tượng và sau đó tôi trả lại kết quả hành động với đối tượng trang mới được sửa đổi và mong đợi hộp văn bản có liên quan (được kết xuất bằng cách sử dụng <%= Html.TextBox("seoTitle", page.SeoTitle)%>) được cập nhật ... nhưng than ôi nó hiển thị giá trị từ mô hình cũ đã được tải.

Tôi đã làm việc xung quanh nó bằng cách sử dụng ModelState.Clear()nhưng tôi cần biết tại sao / làm thế nào nó hoạt động để tôi không chỉ làm điều đó một cách mù quáng.

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>

Noob AspMVC, nếu nó muốn bộ nhớ cache dữ liệu cũ, sau đó điểm trong việc đưa ra mô hình để sử dụng một lần nữa là những gì: @ tôi đã có cùng một vấn đề, thanks a lot bro
deadManN

Câu trả lời:


135

Tôi nghĩ đó là một lỗi trong MVC. Tôi đã vật lộn với vấn đề này trong nhiều giờ hôm nay.

Đưa ra điều này:

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

Chế độ xem hiển thị với mô hình ban đầu, bỏ qua các thay đổi. Vì vậy, tôi nghĩ, có lẽ nó không thích tôi sử dụng cùng một mô hình, vì vậy tôi đã thử như thế này:

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

Và vẫn là chế độ xem kết xuất với mô hình ban đầu. Điều kỳ lạ là, khi tôi đặt một breakpoint trong khung nhìn và kiểm tra mô hình, nó có giá trị thay đổi. Nhưng luồng phản hồi có các giá trị cũ.

Cuối cùng, tôi phát hiện ra công việc tương tự xung quanh bạn đã làm:

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

Hoạt động như mong đợi.

Tôi không nghĩ đây là một "tính năng", phải không?


33
Gần như đã làm điều tương tự như bạn. Tuy nhiên, phát hiện ra đây không phải là một lỗi. Đó là do thiết kế: Một con bọ? EditorFor và DisplayFor không hiển thị cùng một giá trịHtml Helpers của ASP.NET MVC Render giá trị sai
Metro Smurf

8
Trời đất, tôi đã dành 2 giờ để chiến đấu với nó. Cảm ơn vì đã đăng câu trả lời này!
Andrey Agibalov,

37
điều này vẫn đúng và rất nhiều người, bao gồm cả tôi, đang mất rất nhiều thời gian vì điều này. lỗi hoặc do thiết kế, tôi không quan tâm, đó là "bất ngờ".
Proviste

7
Tôi đồng ý với @Proviste, tôi hy vọng "tính năng" này sẽ bị xóa trong tương lai
Ben

8
Tôi chỉ dành bốn giờ cho việc này. Xấu xí.
Brian MacKay

46

Cập nhật:

  • Đây không phải là một lỗi.
  • Vui lòng ngừng quay lại View()từ một hành động ĐĂNG. Sử dụng PRG thay thế và chuyển hướng đến GET nếu hành động thành công.
  • Nếu bạn đang trả về một View()hành động POST, hãy làm điều đó để xác thực biểu mẫu và thực hiện theo cách MVC được thiết kế bằng cách sử dụng trình trợ giúp tích hợp sẵn. Nếu bạn làm theo cách này thì bạn không cần sử dụng.Clear()
  • Nếu bạn đang sử dụng hành động này để trả về ajax cho một SPA , hãy sử dụng bộ điều khiển api web và quên điều ModelStateđó vì bạn không nên sử dụng nó.

Câu trả lời cũ:

ModelState trong MVC được sử dụng chủ yếu để mô tả trạng thái của một đối tượng mô hình chủ yếu liên quan đến việc đối tượng đó có hợp lệ hay không. Hướng dẫn này sẽ giải thích rất nhiều.

Nói chung, bạn không cần xóa ModelState vì nó được công cụ MVC duy trì cho bạn. Xóa nó theo cách thủ công có thể gây ra kết quả không mong muốn khi cố gắng tuân thủ các phương pháp hay nhất về xác thực MVC.

Có vẻ như bạn đang cố gắng đặt giá trị mặc định cho tiêu đề. Điều này nên được thực hiện khi đối tượng mô hình được khởi tạo (lớp miền ở đâu đó hoặc trong chính đối tượng - ctor không tham số), trên hành động get sao cho nó đi xuống trang lần đầu tiên hoặc hoàn toàn trên máy khách (thông qua ajax hoặc thứ gì đó) để nó xuất hiện như thể người dùng đã nhập và nó trở lại với bộ sưu tập biểu mẫu đã đăng. Một số cách tiếp cận của bạn thêm giá trị này trên nhận của một bộ sưu tập các hình thức (trong hành động POST // Edit) đang gây ra hành vi kỳ lạ này có thể dẫn đến một sự .Clear() xuất hiện để làm việc cho bạn. Tin tôi đi - bạn không muốn sử dụng cái rõ ràng. Hãy thử một trong những ý tưởng khác.


1
Nó giúp tôi suy nghĩ lại về lớp dịch vụ của mình một chút (rên rỉ nhưng thx) nhưng cũng như với rất nhiều thứ trên mạng, nó nghiêng nhiều về quan điểm sử dụng ModelState để xác thực.
Mr Grok

Bổ sung thêm thông tin cho câu hỏi để hiển thị tại sao tôi đặc biệt quan tâm đến ModelState.Clear () và lý do cho truy vấn của tôi
Ông grok

5
Tôi không thực sự mua đối số này để ngừng trả về Chế độ xem (...) từ một hàm [HttpPost]. Nếu bạn đang ĐĂNG nội dung qua ajax và sau đó cập nhật tài liệu bằng PartialView kết quả, MVC ModelState đã được hiển thị là không chính xác. Cách giải quyết duy nhất tôi đã tìm thấy là Xóa nó trong phương pháp bộ điều khiển.
Aaron Hudon vào

@AaronHudon PRG được thiết lập khá tốt.
Matt Kocaj

Nếu tôi ĐĂNG bằng một lệnh gọi AJAX, tôi có thể chuyển hướng đến hành động GET và trả về chế độ xem đầy mô hình như OP muốn, tất cả đều không đồng bộ không?
MyiEye

17

Nếu bạn muốn xóa một giá trị cho một trường riêng lẻ thì tôi thấy kỹ thuật sau hữu ích.

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

Lưu ý: Thay đổi "Khóa" thành tên của trường mà bạn muốn đặt lại.


Tôi không biết tại sao điều này lại hoạt động khác với tôi (có lẽ là MVC4)? Nhưng tôi cũng phải làm model.Key = "" sau đó. Cả hai dòng là bắt buộc.
TTT

Tôi muốn khen bạn về nhận xét xóa @PeterGluck. Nó tốt hơn là xóa mô hình hoàn chỉnh (vì tôi có lỗi trên một số trường mà tôi muốn giữ lại).
Tjab

6

Về cơ bản, ModelState giữ Trạng thái hiện tại của mô hình về mặt xác thực, nó nắm giữ

ModelErrorCollection: Biểu diễn các lỗi khi mô hình cố gắng liên kết các giá trị. Ví dụ.

TryUpdateModel();
UpdateModel();

hoặc như một tham số trong ActionResult

public ActionResult Create(Person person)

ValueProviderResult : Giữ thông tin chi tiết về liên kết đã cố gắng với mô hình. Ví dụ. AttemptedValue, Culture, RawValue .

Phương thức Clear () phải được sử dụng thận trọng vì nó có thể dẫn đến kết quả không được mong đợi. Và bạn sẽ mất một số thuộc tính tốt đẹp của ModelState như AttemptedValue, điều này được MVC sử dụng trong nền để tạo lại các giá trị biểu mẫu trong trường hợp có lỗi.

ModelState["a"].Value.AttemptedValue

1
hmmm ... Đó có thể là nơi tôi nhận ra vấn đề bởi vẻ ngoài của nó. Tôi đã kiểm tra giá trị của thuộc tính Model.SeoTitle và nó đã thay đổi nhưng giá trị đã thử thì không. Có vẻ như nó đang gắn giá trị vào như thể có lỗi trên trang mặc dù không có lỗi nào (đã kiểm tra Từ điển ModelState và không có lỗi nào).
Mr Grok

6

Tôi đã có một trường hợp mà tôi muốn cập nhật mô hình của biểu mẫu đã gửi và không muốn 'Chuyển hướng đến hành động' vì lý do hiệu suất. Các giá trị trước đây của các trường ẩn vẫn được giữ lại trên mô hình cập nhật của tôi - gây ra đủ loại vấn đề !.

Một vài dòng mã đã sớm xác định các phần tử trong ModelState mà tôi muốn loại bỏ (sau khi xác thực), vì vậy các giá trị mới được sử dụng trong biểu mẫu: -

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

5

Rất nhiều người trong chúng ta dường như đã bị ảnh hưởng bởi điều này, và mặc dù lý do điều này xảy ra có lý, tôi cần một cách để đảm bảo rằng giá trị trên Model của tôi được hiển thị, chứ không phải ModelState.

Một số đã đề xuất ModelState.Remove(string key), nhưng không rõ ràng điều gì keynên làm, đặc biệt là đối với các mô hình lồng nhau. Đây là một vài phương pháp tôi đã đưa ra để hỗ trợ việc này.

Các RemoveStateForphương pháp sẽ mất một ModelStateDictionary, một mô hình, và một biểu thức cho các tài sản mong muốn, và loại bỏ nó. HiddenForModelcó thể được sử dụng trong Chế độ xem của bạn để tạo trường nhập ẩn chỉ sử dụng giá trị từ Mô hình, bằng cách xóa mục nhập ModelState của nó trước. (Điều này có thể dễ dàng được mở rộng cho các phương pháp mở rộng trình trợ giúp khác).

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

Gọi từ một bộ điều khiển như thế này:

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

hoặc từ một cái nhìn như thế này:

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

Nó sử dụng System.Web.Mvc.ExpressionHelperđể lấy tên của thuộc tính ModelState.


1
Rất đẹp! Giữ một tab trên này cho chức năng ExpressionHelper.
Gerard ONeill

4

Tôi muốn cập nhật hoặc đặt lại một giá trị nếu nó không xác thực và gặp phải sự cố này.

Câu trả lời dễ dàng, ModelState.Remove, là .. có vấn đề .. bởi vì nếu bạn đang sử dụng trình trợ giúp, bạn không thực sự biết tên (trừ khi bạn tuân theo quy ước đặt tên). Trừ khi có lẽ bạn tạo một hàm mà cả người trợ giúp tùy chỉnh và bộ điều khiển của bạn đều có thể sử dụng để đặt tên.

Tính năng này lẽ ra phải được triển khai dưới dạng một tùy chọn trên trình trợ giúp, trong đó theo mặc định, tính năng này không thực hiện điều này, nhưng nếu bạn muốn đầu vào không được chấp nhận hiển thị lại, bạn có thể nói như vậy.

Nhưng ít nhất bây giờ tôi đã hiểu vấn đề;).


Tôi cần phải làm chính xác điều này; xem các phương pháp của tôi mà tôi đã đăng dưới đây đã giúp tôi tìm Remove()được chìa khóa chính xác.
Tobias J

0

Cuối cùng thì cũng hiểu rồi. ModelBinder tùy chỉnh của tôi chưa được đăng ký và thực hiện điều này:

var mymsPage = new MyCmsPage();

NameValueCollection frm = controllerContext.HttpContext.Request.Form;

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

Vì vậy, một cái gì đó mà liên kết mô hình mặc định đang thực hiện phải gây ra sự cố. Không chắc chắn điều gì, nhưng vấn đề của tôi ít nhất đã được khắc phục hiện tại rằng chất kết dính mô hình tùy chỉnh của tôi đang được đăng ký.


Tôi không có kinh nghiệm với ModelBinder tùy chỉnh, cái mặc định phù hợp với nhu cầu của tôi cho đến nay =).
JOBG

0

Nói chung, khi bạn thấy mình đang chống lại các thông lệ tiêu chuẩn khung, thì đã đến lúc bạn nên xem xét lại cách tiếp cận của mình. Trong trường hợp này, hành vi của ModelState. Ví dụ: khi bạn không muốn trạng thái mô hình sau POST, hãy xem xét chuyển hướng đến 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);
}

CHỈNH SỬA để trả lời bình luận văn hóa:

Đây là những gì tôi sử dụng để xử lý một ứng dụng MVC đa văn hóa. Đầu tiên, các lớp con của trình xử lý định tuyến:

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
}

Và đây là cách tôi sắp xếp các tuyến đường. Sau khi tạo các tuyến, tôi thêm vào trước chất phụ của mình (example.com/subagent1, example.com/subagent2, v.v.) rồi đến mã văn hóa. Nếu tất cả những gì bạn cần là văn hóa, chỉ cần xóa chất phụ khỏi trình xử lý tuyến và tuyến.

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

    }

Bạn rất đúng khi đề xuất thực hành ĐÚNG ĐỔI, trên thực tế, tôi làm điều này cho hầu hết mọi hành động đăng. Tuy nhiên, tôi có một nhu cầu rất đặc biệt: tôi có một biểu mẫu bộ lọc ở đầu trang, ban đầu nó được gửi bằng get. Nhưng tôi đã gặp sự cố với trường ngày tháng không bị ràng buộc và sau đó phát hiện ra rằng các yêu cầu GET không mang văn hóa xung quanh (tôi sử dụng tiếng Pháp cho ứng dụng của mình), vì vậy tôi đã phải chuyển yêu cầu thành POST để liên kết thành công ngày của mình. Sau đó, đến vấn đề này, tôi là một chút khó khăn cô ấy ..
Souhaieb Besbes

@SouhaiebBesbes Xem các cập nhật của tôi cho biết cách tôi xử lý văn hóa.
B2K

@SouhaiebBesbes có lẽ đơn giản hơn một chút là lưu trữ văn hóa của bạn trong TempData. Xem stackoverflow.com/questions/12422930/…
B2K

0

Chà, điều này dường như hoạt động trên Trang Razor của tôi và thậm chí chưa bao giờ thực hiện một chuyến đi vòng quanh tệp .cs. Đây là cách html cũ. Nó có thể hữu ích.

<input type="reset" value="Reset">
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.