Possible bug in ASP.NET MVC with form values being replaced

后端 未结 12 676
抹茶落季
抹茶落季 2020-11-29 00:41

I appear to be having a problem with ASP.NET MVC in that, if I have more than one form on a page which uses the same name in each one, but as different types (radio/hidden/e

12条回答
  •  执念已碎
    2020-11-29 01:00

    Same as others I would have expected the ModelState to be used to fill the Model and as we explicitly use the Model in expressions in the view, it should use the Model and not ModelState.

    It's a design choice and I do get why: if validations fail, the input value might not be parseable to the datatype in the model and you still want to render whatever wrong value the user typed, so it's easy to correct it.

    The only thing I don't understand is: why isn't it by design that the Model is used, which is set explicitly by the developer and if a validation error occurred, the ModelState is used.

    I have seen many people using workarounds like

    • ModelState.Clear(): Clears all ModelState values, but basically disables usage of default validation in MVC
    • ModelState.Remove("SomeKey"): Same as ModelState.Clear() but needs micromanagement of ModelState keys, which is too much work and it doesn't feel right with the auto binding feature from MVC. Feels like 20 years back when we were also managing Form and QueryString keys.
    • Rendering HTMLthemselves: too much work, detail and throws away the HTML Helper methods with the additional features. An example: Replace @Html.HiddenFor by . Or replace @Html.DropDownListFor by ...
    • Create custom HTML Helpers to replace default MVC HTML Helpers to avoid the by-design issue. This is a more generic approach then rendering your HTML, but still requires more HTML+MVC knowledge or decompiling System.Web.MVC to still keep all other features but disable ModelState precedence over Model.
    • Apply the POST-REDIRECT-GET Pattern: this is easy in some environments, but harder in the ones with more interaction/complexity. This pattern has it's pros and cons and you shouldn't be forced to apply this pattern because of a by-design choice of ModelState over Model.

    Issue

    So the issue is that the Model is filled from ModelState and in the view, we set explicitly to use the Model. Everybody expects the Model value (in case it changed) to be used unless there's a validation error; then the ModelState can be used.

    Currently, in the MVC Helper extensions, the ModelState value gets precedence over the Model value.

    Solution

    So the actual fix for this issue should be: for each expression to pull the Model value the ModelState value should be removed if there is no validation error for that value. If there's a validation error for that input control the ModelState value shouldn't be removed and it will be used like normal. I think this solves the issue exactly, which is better than most workarounds.

    The code is here:

        /// 
        /// Removes the ModelState entry corresponding to the specified property on the model if no validation errors exist. 
        /// Call this when changing Model values on the server after a postback, 
        /// to prevent ModelState entries from taking precedence.
        /// 
        public static void RemoveStateFor(this HtmlHelper helper,  
            Expression> expression)
        {
            //First get the expected name value. This is equivalent to helper.NameFor(expression)
            string name = ExpressionHelper.GetExpressionText(expression);
            string fullHtmlFieldName = helper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
    
            //Now check whether modelstate errors exist for this input control
            ModelState modelState;
            if (!helper.ViewData.ModelState.TryGetValue(fullHtmlFieldName, out modelState) ||
                modelState.Errors.Count == 0)
            {
                //Only remove ModelState value if no modelstate error exists,
                //so the ModelState will not be used over the Model
                helper.ViewData.ModelState.Remove(name);
            }
        }
    

    And then we create our own HTML Helper extensions todo this before calling the MVC extensions:

        public static MvcHtmlString TextBoxForModel(this HtmlHelper htmlHelper,
            Expression> expression,
            string format = "",
            Dictionary htmlAttributes = null)
        {
            RemoveStateFor(htmlHelper, expression);
            return htmlHelper.TextBoxFor(expression, format, htmlAttributes);
        }
    
        public static IHtmlString HiddenForModel(this HtmlHelper htmlHelper,
            Expression> expression)
        {
            RemoveStateFor(htmlHelper, expression);
            return htmlHelper.HiddenFor(expression);
        }
    

    This solution removes the issue but doesn't require you to decompile, analyze, and rebuild whatever MVC is offering you normally (don't forget also managing changes over-time, browser differences, etc.).

    I think the logic of "Model value unless validation error then ModelState" should have been by-design. If it was, it wouldn't have bitten so many people, but still covered what MVC was intended todo.

提交回复
热议问题