سؤال

هل يمكن لأي شخص أن يقدم لي تعريفًا موجزًا ​​لدور ModelState في Asp.net MVC (أو رابطًا إلى واحد).وعلى وجه الخصوص، أحتاج إلى معرفة المواقف التي يكون من الضروري أو المرغوب فيها الاتصال بها ModelState.Clear().

بت مفتوح النهاية هاه...آسف، أعتقد أنه قد يكون من المفيد أن أخبرك بما أفعله تمامًا:

لدي إجراء تحرير على وحدة تحكم تسمى "الصفحة".عندما أرى النموذج الخاص بتغيير تفاصيل الصفحة لأول مرة، يتم تحميل كل شيء بشكل جيد (الربط بكائن "MyCmsPage").ثم أقوم بالنقر فوق الزر الذي يقوم بإنشاء قيمة لأحد حقول كائن MyCmsPage (MyCmsPage.SeoTitle).يقوم بإنشاء الكائن بشكل جيد وتحديثه، ثم أقوم بإرجاع نتيجة الإجراء باستخدام كائن الصفحة المعدل حديثًا وأتوقع مربع النص ذي الصلة (يتم تقديمه باستخدام <%= Html.TextBox("seoTitle", page.SeoTitle)%>) ليتم تحديثه ...ولكن للأسف فإنه يعرض القيمة من النموذج القديم الذي تم تحميله.

لقد عملت حول هذا باستخدام ModelState.Clear() لكني أريد أن أعرف لماذا/كيف نجح الأمر حتى لا أفعل ذلك بشكل أعمى.

تحكم الصفحة:

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

أسبكس:

<%@ 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>
هل كانت مفيدة؟

المحلول

أعتقد أن هناك خطأ في MVC.لقد ناضلت مع هذه المشكلة لساعات اليوم.

ونظرا لهذا:

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

يتم عرض العرض مع النموذج الأصلي، مع تجاهل التغييرات.لذلك فكرت، ربما لا أحب أن أستخدم نفس النموذج، لذلك حاولت بهذه الطريقة:

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

وما زال العرض يظهر مع النموذج الأصلي.الأمر الغريب هو أنه عندما أضع نقطة توقف في العرض وأفحص النموذج، تكون لها القيمة المتغيرة.لكن دفق الاستجابة له القيم القديمة.

في النهاية اكتشفت نفس العمل الذي قمت به:

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

يعمل كما هو متوقع.

لا أعتقد أن هذه "ميزة"، أليس كذلك؟

نصائح أخرى

تحديث:

  • هذه ليست علة.
  • من فضلك توقف عن العودة View() من إجراء ما بعد.يستخدم باراغون بدلاً من ذلك وأعد التوجيه إلى GET إذا كان الإجراء ناجحًا.
  • اذا أنت نكون يعود أ View() من إجراء POST، قم بذلك للتحقق من صحة النموذج، وقم بذلك بالطريقة نفسها تم تصميم MVC باستخدام المساعدين المدمجين.إذا قمت بذلك بهذه الطريقة فلن تحتاج إلى استخدامها .Clear()
  • إذا كنت تستخدم هذا الإجراء لإرجاع ajax لـ a منتجع صحي, ، استخدم وحدة تحكم Web API ونسيان الأمر ModelState لأنك لا ينبغي أن تستخدمه على أي حال.

الجواب القديم:

يتم استخدام ModelState في MVC بشكل أساسي لوصف حالة كائن النموذج فيما يتعلق إلى حد كبير بما إذا كان هذا الكائن صالحًا أم لا. هذا البرنامج التعليمي يجب أن يفسر الكثير.

بشكل عام، لن تحتاج إلى مسح ModelState حيث يتم صيانته بواسطة محرك MVC نيابةً عنك.قد يؤدي مسحها يدويًا إلى نتائج غير مرغوب فيها عند محاولة الالتزام بأفضل ممارسات التحقق من صحة MVC.

يبدو أنك تحاول تعيين قيمة افتراضية للعنوان.يجب أن يتم ذلك عندما يتم إنشاء كائن النموذج (طبقة المجال في مكان ما أو في الكائن نفسه - ctor بدون معلمات)، في إجراء get بحيث ينتقل إلى الصفحة في المرة الأولى أو بالكامل على العميل (عبر ajax أو شيء من هذا القبيل) بحيث يبدو كما لو أن المستخدم قام بإدخاله ويعود مع مجموعة النماذج المنشورة.بعض الطرق التي تتبعها في إضافة هذه القيمة عند استلام مجموعة النماذج (في إجراء POST // تحرير) تتسبب في هذا السلوك الغريب الذي قد يؤدي إلى .Clear() الظهور للعمل من أجلك.ثق بي - أنت لا تريد استخدام الشفاف.جرب إحدى الأفكار الأخرى.

إذا كنت تريد مسح قيمة لحقل فردي، فقد وجدت التقنية التالية مفيدة.

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

ملحوظة:قم بتغيير "المفتاح" إلى اسم الحقل الذي تريد إعادة تعيينه.

حسنًا، يحمل ModelState بشكل أساسي الحالة الحالية للنموذج من حيث التحقق من الصحة، فهو يحمل

مجموعة نماذج الأخطاء: قم بتمثيل الأخطاء عندما يحاول النموذج ربط القيم.السابق.

TryUpdateModel();
UpdateModel();

أو مثل المعلمة في ActionResult

public ActionResult Create(Person person)

ValueProviderResult:احتفظ بالتفاصيل حول محاولة الربط بالنموذج.السابق. قيمة المحاولة، الثقافة، القيمة الخام.

يجب استخدام طريقة Clear() بحذر لأنها قد تؤدي إلى نتائج غير متوقعة.وسوف تفقد بعض الخصائص الرائعة لـ ModelState مثل AttemptedValue، والتي يستخدمها MVC في الخلفية لإعادة ملء قيم النموذج في حالة حدوث خطأ.

ModelState["a"].Value.AttemptedValue

كان لدي مثال حيث أردت تحديث نموذج النموذج المقدم، ولم أرغب في "إعادة التوجيه إلى الإجراء" لأسباب تتعلق بالأداء.تم الاحتفاظ بالقيم السابقة للحقول المخفية في النموذج المحدث الخاص بي - مما تسبب في جميع أنواع المشكلات!.

وسرعان ما حددت بضعة أسطر من التعليمات البرمجية العناصر داخل ModelState التي أردت إزالتها (بعد التحقق من الصحة)، لذلك تم استخدام القيم الجديدة في النموذج: -

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

حسنًا، يبدو أن الكثير منا قد تعرض للعض من هذا، وعلى الرغم من أن سبب حدوث ذلك يبدو منطقيًا، إلا أنني كنت بحاجة إلى طريقة للتأكد من ظهور القيمة في النموذج الخاص بي، وليس ModelState.

وقد اقترح البعض ModelState.Remove(string key), ، ولكن ليس من الواضح ما key ينبغي أن يكون، وخاصة بالنسبة للنماذج المتداخلة.فيما يلي طريقتان توصلت إليهما للمساعدة في ذلك.

ال RemoveStateFor سوف تستغرق الطريقة أ ModelStateDictionary, ونموذج وتعبير للخاصية المطلوبة وإزالته. HiddenForModel يمكن استخدامه في طريقة العرض الخاصة بك لإنشاء حقل إدخال مخفي باستخدام القيمة من النموذج فقط، وذلك عن طريق إزالة إدخال ModelState الخاص به أولاً.(يمكن توسيع هذا بسهولة لطرق الامتداد المساعد الأخرى).

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

استدعاء من وحدة تحكم مثل هذا:

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

أو من وجهة نظر مثل هذا:

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

يستخدم System.Web.Mvc.ExpressionHelper للحصول على اسم خاصية ModelState.

كنت أرغب في تحديث قيمة أو إعادة تعيينها إذا لم يتم التحقق من صحتها تمامًا، وواجهت هذه المشكلة.

الإجابة السهلة، ModelState.Remove، هي..إشكالية..لأنه إذا كنت تستخدم مساعدين، فأنت لا تعرف الاسم حقًا (إلا إذا التزمت باصطلاح التسمية).ما لم تقم بإنشاء وظيفة خاصة بك مخصص يمكن استخدام المساعد ووحدة التحكم الخاصة بك للحصول على اسم.

يجب أن يتم تنفيذ هذه الميزة كخيار على المساعد، حيث يتم تنفيذها افتراضيًا لا قم بذلك، ولكن إذا كنت تريد إعادة عرض الإدخال غير المقبول، فيمكنك قول ذلك.

لكن على الأقل أفهم المشكلة الآن ;).

حصلت عليه في النهاية.My Custom ModelBinder الذي لم يتم تسجيله ويقوم بما يلي:

var mymsPage = new MyCmsPage();

NameValueCollection frm = controllerContext.HttpContext.Request.Form;

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

لذا فإن الشيء الذي كان يفعله ربط النموذج الافتراضي هو الذي تسبب في المشكلة.لست متأكدًا من ذلك، ولكن مشكلتي تم إصلاحها على الأقل الآن بعد أن تم تسجيل رابط النموذج المخصص الخاص بي.

بشكل عام، عندما تجد نفسك تقاتل ضد الممارسات القياسية لإطار العمل، فقد حان الوقت لإعادة النظر في نهجك.في هذه الحالة، سلوك ModelState.على سبيل المثال، عندما لا تريد حالة النموذج بعد POST، فكر في إعادة التوجيه إلى 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);
}

تم التعديل للإجابة على تعليق الثقافة:

هذا ما أستخدمه للتعامل مع تطبيق MVC متعدد الثقافات.أولاً الفئات الفرعية لمعالج المسار:

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
}

وهنا كيف أقوم بتوصيل الطرق.بعد إنشاء المسارات، أقوم بإرفاق الوكيل الفرعي الخاص بي (example.com/subagent1، example.com/subagent2، إلخ) ثم رمز الثقافة.إذا كان كل ما تحتاجه هو الثقافة، فما عليك سوى إزالة الوكيل الفرعي من معالجات المسار والمسارات.

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

    }

حسنًا، يبدو أن هذا يعمل على صفحة Razor الخاصة بي ولم يقم أبدًا برحلة ذهابًا وإيابًا إلى ملف .cs.هذه طريقة HTML قديمة.قد يكون مفيدا.

<input type="reset" value="Reset">
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top