MVC Post Causes QueryString to Be Lost on Reload of Same View
Asked Answered
T

8

10

Please let me explain the setup.

I have a Change Password Controller/Action and View. Here are the Action Signatures in my Account Controller:

public ActionResult ChangePassword(ChangePasswordMessageId? message)

[HttpPost]
public ActionResult ChangePassword(ChangePasswordViewModel model)

When change password is first loaded, I have some data in the querystring. Here is an example:

https://www.mywebsite.com/Account/ChangePassword?mobile=1

Here is the Form declaration from the view.

@using (Html.BeginForm("ChangePassword", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
    @Html.AntiForgeryToken()

The form is submitted with a simple submit button:

<div class="form-group">
  <div class="col-md-offset-2 col-md-4">
    <input type="submit" value="Change Password" class="btn btn-primary btn-block" />
  </div>
</div>

The form has 3 fields: Current Password, New Password, and Confirm Password. If the user fills in all the data correctly, and passes all the client side validation the form works fine. Everything works fine except for one use case.

Suppose the user enters an Old Password value that is not correct. When I get to the HTTPPOST ChangePassword action above, it will fail. This is what that code looks like.

[HttpPost]
            public ActionResult ChangePassword(ChangePasswordViewModel model)
            {   
                if (ModelState.IsValid)
                {
                    try
                    {
                        MembershipUser user = Membership.GetUser();
        //The NEXT line is the one that fails if they supply the wrong Old Password value.
        //The code then falls to the catch condition below.
                        bool changePassword = user.ChangePassword(model.OldPassword, model.NewPassword);
                        if (changePassword)
                        {
                            string path = Url.Action("ChangePassword", new { Message = ChangePasswordMessageId.ChangePasswordSuccess });
                            temp = Request.UrlReferrer.ToString();
                            pos = temp.IndexOf("?");
                            if (pos > 0) path += "&" + temp.Substring(pos + 1);
                            return RedirectToLocal(path);    
                       }
                        else
                        {
                            ModelState.AddModelError("", "Change Password failed.");
                        }
                    }
                    catch //(Exception ex)
                    {
                        ModelState.AddModelError("", "Change Password failed.");
                        //ModelState.AddModelError("", ex.Message);
                    }
                }

                // If we got this far, something failed, redisplay form
    //The original query string will be gone. The URLwill now only show
    //https://www.mywebsite.com/Account/ChangePassword
                return View(model);
            }

Is there away to call "return View(model);" so that the original query string will still be there? I need to maintain the query string from page to page. I have it working everywhere except for this one use case.

Thanks!

Teplitz answered 17/7, 2014 at 12:45 Comment(2)
Why not using an ajax call in this case ?Ledet
Thanks. I actually thought of that and tried it. However, it was not as simple as changing HTML.BeginForm to AJAX.BeginForm. By default, when the HTTPPost action returns the View(model) the page just did nothing. I would have to write more code to update the display. I am looking for a more generic solution that I can apply everwhere this could occur.Teplitz
B
8

If somebody is still looking for this solution, try not supplying controller and action names.

 @using (Html.BeginForm(null, FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
    {
        @Html.AntiForgeryToken()
Burgett answered 19/3, 2015 at 22:25 Comment(0)
S
1

One way you could do it is to make your Mobile flag part of your View Model. And, in your Get controller action, make sure you get the Mobile flag from your query string when you populate your View Model.

On your form, create a hidden input field for the fields you want posted back. E.g.:

@Html.Hidden(Model.Mobile)

On post, the Mobile value will be set correctly in your ChangePasswordViewModel so you have access to it.

Schematize answered 17/7, 2014 at 13:28 Comment(0)
A
1
return RedirectToAction("Index", Request.QueryString.ToRouteValues());
Antifederalist answered 29/3, 2017 at 23:2 Comment(1)
Is 'ToRouteValues' your own extension method?Nahshunn
B
1

My code is in VB, in the view, create a helper function to load all URL query parameters into the form:

@Functions
Public Function GetRouteValues() As RouteValueDictionary
    Dim RouteValues As New RouteValueDictionary
    For Each Qstr As String In Request.QueryString
        RouteValues.Add(Qstr, Request.QueryString.GetValues(Qstr).FirstOrDefault)
    Next
    Return RouteValues
End Function
End Functions

then declare the form head as following

@Using (Html.BeginForm("MultipleHandler", "Files", GetRouteValues, FormMethod.Post))

This way, you passed your collection of query string to the action.

In the action, use the same function:

Private Function AllRouteVlaues() As RouteValueDictionary
        Dim RouteValues As New RouteValueDictionary
        For Each Qstr As String In Request.QueryString
            RouteValues.Add(Qstr, Request.QueryString.GetValues(Qstr).FirstOrDefault)
        Next
        Return RouteValues
    End Function

Usage:

Return RedirectToAction("Index", AllRouteVlaues)

Hope that helps :)

Barbed answered 5/8, 2017 at 23:22 Comment(0)
V
0

Try this:

using (Html.BeginForm("Action", "Controller",  new { Var1 = (string)ViewBag.Var1, Var2 = (string)ViewBag.Var2, Var3 = (string)ViewBag.Var3 }, FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
                    {
Ventriloquist answered 28/3, 2019 at 21:8 Comment(0)
B
0

Using Tag Helpers in MVC 6, you can add your query string in the form data using the following code. Although the query string will be lost between reloads, it makes ReturnURL scenarios work. I don't like to use RedirectToAction in this case because it will create an additional request using a 302 redirect. It is much slower.

<form asp-controller="Account" asp-action="Login"
     asp-route-returnurl="@ViewData["ReturnUrl"]"
     method="post" class="form-horizontal" role="form">

See the documentation.

Bruch answered 2/12, 2022 at 6:37 Comment(0)
S
0

My project targets .NET 8 and utilizes Razor Pages.

I resolved this issue by leveraging Html.BeginForm, which allows me to set the route values directly from the model.

Simple example:

@using (Html.BeginForm("MyAction", "MyController",
                        routeValues: new {token = Model.Token, email = Model.Email}, @*<--- must set the routeValues *@
                        method: FormMethod.Post))
     {
     }

Complete example:

@model ResetPasswordDataModel;

<div class="container">
    <h2>Reset Password</h2>
@using (Html.BeginForm("ResetPassword", "Account",
                        routeValues: new {token = Model.Token, email = Model.Email},
                        method: FormMethod.Post))
     {
        @Html.AntiForgeryToken()
        <div class="form-group">
            <label for="password">New Password</label>
            <input asp-for="@Model.Password" type="password" id="password" name="password" required>
        </div>

        <div class="form-group">
            <label for="confirm_password">Confirm Password</label>
            <input asp-for="@Model.ConfirmPassword" type="password" id="confirm_password" name="confirmPassword" required>
        </div>

        <div asp-validation-summary="All" class="text-danger"></div>

        <input type="hidden" asp-for="@Model.Email" class="form-control" />
        <input type="hidden" asp-for="@Model.Token" class="form-control" />
        <input type="submit" value="Reset Password" class="btn">
}
</div>

Post action:

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ResetPassword(ResetPasswordDataModel model, CancellationToken cancellationToken)
{
    if (!ModelState.IsValid)
    {
        return View(model); // pass the model to the view
    }
    
    // Continue the implementation here...
}
Secluded answered 15/2, 2024 at 16:26 Comment(0)
S
-1

You need to include the query-string parameters in the new request "somehow".
On way would be the following (this is also how MS does it with the ReturnUrl in their examples):

public ActionResult ChangePassword(ChangePasswordMessageId? message. int mobile = 0)
{
    // your code
    ViewBag.Mobile = mobile;
    return View();
}
[HttpPost]
public ActionResult ChangePassword(ChangePasswordViewModel model, int mobile = 0)
{
    // your code
    ViewBag.Mobile = mobile;
    return View(model);
}

and

@using (Html.BeginForm("ChangePassword", "Account", FormMethod.Post, 
    new { @class = "form-horizontal", role = "form", mobile = @ViewBag.Mobile }))
Seam answered 17/7, 2014 at 13:2 Comment(2)
Thanks, I was hoping for a generic solution so I do not have to find every case in my code that this could occur.Teplitz
You COULD use Request.Url and always append the query string, but that sounds dirty to me - maybe save such settings in the session?Seam

© 2022 - 2025 — McMap. All rights reserved.