3

Going through the scaffolded MVC 5 template with Individual Accounts authentication, i've stumbled on a behavior i can't get my head around.

Given a request url

http://localhost:53487/Account/ResetPassword?userId=4&code=T634Hfv%2BxMAlo2XjdLV6a%2Bd1%2BxGsfdiQiKRW0Nh2fB3I1U3S%2BNdXU4ixHC9uJ5F5PSRMZkQgV907CDH0x3aQPSdFliXJqD7nrjk3TLnOTawPeO8CJjk5OEyYijVur1i1Fr7DE7nmaDD93I000fXbQA%3D%3D

and action method in AccountController

[AllowAnonymous]
public ActionResult ResetPassword(string code)
{
    return code == null ? View("Error") : View();
}

and the view ResetPassword.cshtml

@model OPLA.Web.Models.ResetPasswordViewModel
@{
    ViewBag.Title = "Reset password";
}

<h2>@ViewBag.Title.</h2>

@using (Html.BeginForm("ResetPassword", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
    @Html.AntiForgeryToken()
    <h4>Reset your password.</h4>
    <hr />
    @Html.ValidationSummary("", new { @class = "text-danger" })    
    @Html.HiddenFor(model => model.Code)
    <div class="form-group">
        @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.PasswordFor(m => m.Password, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" class="btn btn-default" value="Reset" />
        </div>
    </div>
}

and viewmodel ResetPasswordViewModel

public class ResetPasswordViewModel
{
    [Required]
    [EmailAddress]
    [Display(Name = "Email")]
    public string Email { get; set; }

    [Required]
    [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; }

    public string Code { get; set; }
}

When the view is loaded, the line @Html.HiddenFor(model => model.Code) produces this html output with Code property of the viewmodel properly filled/bound:

<input id="Code" name="Code" type="hidden" value="T634Hfv+xMAlo2XjdLV6a+d1+xGsfdiQiKRW0Nh2fB3I1U3S+NdXU4ixHC9uJ5F5PSRMZkQgV907CDH0x3aQPSdFliXJqD7nrjk3TLnOTawPeO8CJjk5OEyYijVur1i1Fr7DE7nmaDD93I000fXbQA==">

How did the model binder know the code query string parameter belongs to the Code property of the viewmodel and bound it automatically?

Sang Suantak
  • 5,213
  • 1
  • 27
  • 46

1 Answers1

4

You have a parameter in your method named code. When the method is executed, the value of code is added to ModelState.

Your model also has a property named Code. Your view uses the @Html.HiddenFor() to generate an <input> for that property. All the HtmlHelper methods that generate form controls (except PasswordFor()) determine thevalue for the <input> by reading values in the following order

  1. ModelState
  2. The ViewDataDictionary
  3. The actual value of the property

Because ModelState contains a value for code (its not case sensitive), the value is set from the method parameter (i.e. the query string value).

For a more detailed explanation of why this behavior is by design, refer the 2nd part of TextBoxFor displaying initial value, not the value updated from code.

  • Thanks Stephen, it made sense after going through the linked answer as to the reasoning behind this behavior. I'm curious as to how you know about this behavior, is there any article/documentation? – Sang Suantak Jun 27 '18 at 06:50
  • 1
    Have never seen any official documentation, but there was an article written by one of the MVC team members (Brad Wilson I think?) that discussed it in relation to a change from MVC-2 to MVC-3 (its pretty old now and I do not think I have kept the bookmark, but I'll see if I can find it later today when I get some time) –  Jun 27 '18 at 07:04
  • 1
    Dug up a **[post](https://weblog.west-wind.com/posts/2012/Apr/20/ASPNET-MVC-Postbacks-and-HtmlHelper-Controls-ignoring-Model-Changes)** from Rick Strahl after a bit of googling @Stephen Muecke. – Sang Suantak Jun 27 '18 at 07:20
  • 1
    [This article](http://bradwilson.typepad.com/blog/2010/01/input-validation-vs-model-validation-in-aspnet-mvc.html) was the one I was referring to (actually MVC-1 to MVC-2 change). And if you work through the [source code](https://github.com/aspnet/AspNetWebStack/blob/master/src/System.Web.Mvc/Html/InputExtensions.cs) you an see the priority for setting the `value` attribute –  Jun 27 '18 at 07:26
  • Thanks for taking the trouble of digging up the post :) – Sang Suantak Jun 27 '18 at 07:30
  • Look like Rick's article references Brad's :) But Rick's is not entirely correct since he states that a GET always uses the values from the model (which is not the case as you have discovered). –  Jun 27 '18 at 07:34
  • Also just as a side note, you can omit the hidden input because the `
    ` element contains the value, and the `DefaultModelBinder` reads values from query strings, route values etc in addition to form values
    –  Jun 27 '18 at 07:39
  • Regarding your latest comment, if i remove the hidden field, `Code` property of the viewmodel is not set when form is submitted. – Sang Suantak Jun 27 '18 at 08:41
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/173867/discussion-between-stephen-muecke-and-sang-suantak). –  Jun 27 '18 at 08:42