0

I have a Manage page for the current logged in user. This view loads two partial views. One to update the users registered email address and the other to update the password. The Manage view looks like

@using VisasysNET.Models;
@using Microsoft.AspNet.Identity;
@model VisasysNET.Models.ManageUserViewModel
@{
    ViewBag.Title = "Manage Account";
}

<h2>@ViewBag.Title</h2>
<p class="text-success">@ViewBag.StatusMessage</p>
<div class="row">
    <div class="col-md-12">
        @Html.Partial("_ChangeEmailAddressPartial", Model)
    </div>
</div>
<div class="row">
    <div class="col-md-12">
        @if (ViewBag.HasLocalPassword)
        {
            @Html.Partial("_ChangePasswordPartial")
        }
        else
        {
            @Html.Partial("_SetPasswordPartial")
        }
    </div>
</div>
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

I have an "Update Email/Password" button in my update email partial view and update password partial view respectively. Now, I handle the post from the manage view in the controller via

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Manage(ManageUserViewModel model)
{
    bool hasPassword = HasPassword();
    ViewBag.HasLocalPassword = hasPassword;
    ViewBag.ReturnUrl = Url.Action("Manage");
    if (hasPassword)
    {
        if (ModelState.IsValid)
        {
            IdentityResult result = await UserManager.ChangePasswordAsync(
                User.Identity.GetUserId(), model.OldPassword, model.NewPassword);
            if (result.Succeeded)
            {
                return RedirectToAction("Manage",
                    new { Message = ManageMessageId.ChangePasswordSuccess });
            }
            else
                AddErrors(result);
        }
    }
    else
    {
        // User does not have a password so remove any validation 
        // errors caused by a missing OldPassword field.
        ...
    }
    // If we got this far, something failed, redisplay form.
    return View(model);
}

where

public class ManageUserViewModel
{
    [Required]
    [EmailAddress(ErrorMessage = "Invalid email address.")]
    [DataType(DataType.EmailAddress)]
    [Display(Name = "Email address")]
    public string EmailAddress { get; set; }

    [Required]
    [DataType(DataType.Password)]
    [Display(Name = "Current password")]
    public string OldPassword { get; set; }

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

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

The problem is that if the user clicks the button

<input type="submit" value="Update Password" class="btn btn-primary" />

on the update password view, the ManageUserViewModel.EmailAddress comes back as null and likewise the other way round (if the user clicks the "Update Email Address" button, the password fields come back as null). This is an issue because I then get the validation error messages

Validation Error

In the above I have attempted to update the password. How can I prevent the validation executing for email if the "Update Password" button was pressed and vice-versa?

Thanks for your time.


Request for the Partial Views

Email update partial view is

@using Microsoft.AspNet.Identity
@model VisasysNET.Models.ManageUserViewModel
<p>You're logged in as <strong>@User.Identity.GetUserName()</strong></p>
@using (Html.BeginForm("Manage", "Account", FormMethod.Post,
    new { @class = "form-horizontal", role = "form" }))
{
    @Html.AntiForgeryToken()
    <h4>Change Email Address</h4>
    <hr />
    @Html.ValidationSummary()
    <div class="form-group">
        @Html.LabelFor(m => m.EmailAddress, 
            new 
            { 
                @class = "col-md-2 control-label" 
            })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.EmailAddress, 
                new 
                { 
                    @class = "col-md-10 form-control", 
                    @type = "text",
                    @placeholder = "Email Address",
                    @value = Model.EmailAddress
                })
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" value="Update Email Address" class="btn btn-primary" />
        </div>
    </div>
}

The update password partial is

@using Microsoft.AspNet.Identity
@model VisasysNET.Models.ManageUserViewModel

@*<p>You're logged in as <strong>@User.Identity.GetUserName()</strong></p>*@

@using (Html.BeginForm("Manage", "Account", FormMethod.Post, 
    new { @class = "form-horizontal", role = "form" }))
{
    @Html.AntiForgeryToken()
    <h4>Change Password</h4>
    <hr />
    @Html.ValidationSummary()
    <div class="form-group">
        @Html.LabelFor(m => m.OldPassword, 
        new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.PasswordFor(m => m.OldPassword, 
            new { @class = "form-control" })
        </div>
    </div>

    <div class="form-group">
        @Html.LabelFor(m => m.NewPassword, 
        new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.PasswordFor(m => m.NewPassword, 
            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" value="Change password" class="btn btn-primary" />
        </div>
    </div>
}

Edit #2. Questions on Answer

The Get method is

// GET: /Account/Manage.
public ActionResult Manage(ManageMessageId? message)
{
    ViewBag.StatusMessage =
        message == ManageMessageId.ChangePasswordSuccess ? 
            "Your password has been changed": 
        message == ManageMessageId.SetPasswordSuccess ? 
            "Your password has been set" : 
        message == ManageMessageId.RemoveLoginSuccess ? 
            "The external login was removed" : 
        message == ManageMessageId.ChangeEmailAddressSuccess ?
            "Your email address was successfully updated" :
        message == ManageMessageId.Error ? 
            "An error has occurred." : "";
    ViewBag.HasLocalPassword = HasPassword();
    ViewBag.ReturnUrl = Url.Action("Manage");

    // Get current email address.
    var user = UserManager.FindById(User.Identity.GetUserId());
    ManageUserViewModel model = new ManageUserViewModel();
    model.EmailAddress = user.EmailAddress;
    return View(model);
}

But the view seems to know what OldPassword is and it is placed in the TextBox, this is null obviously, so is this Firefox being clever?

So are you saying that I should pass in an object[] containing two view models one for password, the other for the email stuff?

MoonKnight
  • 23,214
  • 40
  • 145
  • 277

1 Answers1

2

I think you would be better off considering separate view models for these, especially for client side validation, however you can clear ModelState errors for a specific property using

if (ModelState.ContainsKey("Email"))
{
   ModelState["Email"].Errors.Clear();
}
  • Thanks for your reply, it is most appreciated. I am just confused as to best practice here. I have updated the question. If you could advise further that would be awesome... – MoonKnight Nov 03 '14 at 11:52
  • I don't know the details of you app and I don't use `Identity` but at first glance I see a few issues. First you appear to be registering and logging in with a simple name ("Mark") and adding email as an additional property. What ensures the login name is unique? Why not login with the email and have a 'friendly display name' as the additional property? Second, changing the email looks to be a different action than changing the password and you probably should a separate view and a separate view model containing fields for existing, new and confirm (as typically done for a password). –  Nov 03 '14 at 21:43
  • Having a quick look at the default `AccountController` and `AccountViewModels` generated by VS, I'm not sure why that does not suit your needs (adapted to add a 'display name'). You seem to be reinventing the wheel and in the process, losing some of the great features of MVC. For example you cant use client side validation; you need hacks for server side validation; you have a potential security problem (while Mark gets a coffee, his colleague can change the email and Mark's none the wiser) –  Nov 03 '14 at 21:56
  • As for your comment relating to `OldPassword`, you haven't explained the context in which this occurs (so I'm just guessing), but [this answer](http://stackoverflow.com/questions/26654862/textboxfor-displaying-initial-value-not-the-value-updated-from-code) might help to explain how `ModelState` works. –  Nov 03 '14 at 22:02