Submit form with GET method - c#

Alright, I lost the entire morning on this and I'm getting nowhere.
I have a good experience with MVC, been using it since the first betas back in 2008 but I can' t really figure this out.
I substantially have 2 GET methods: the first one renders a form. The second one is the method the form points to. I'm submitting using GET because it's a search form, and I want to have a bookmarkable URL with the parameters.
Something like this
[HttpGet]
public ActionResult DisplayForm()
{
Contract.Ensures(Contract.Result<ActionResult>() != null);
Contract.Ensures(Contract.Result<ActionResult>() is ViewResult);
return this.View();
}
[HttpGet]
public ActionResult Search(MyViewModel viewModel)
{
Contract.Requires<ArgumentNullException>(viewModel != null);
Contract.Ensures(Contract.Result<ActionResult>() != null);
Contract.Ensures(Contract.Result<ActionResult>() is ViewResult || Contract.Result<ActionResult>() is RedirectToRouteResult);
var result = this.validator.Validate(viewModel); //FluentValidation validation
if (!result.IsValid)
{
result.FillModelState(this.ModelState); //extension method that uses AddModelError underneath for ValidationMessageFor helpers on search form
return this.RedirectToAction(c => c.DisplayForm()); //MvcContrib redirect to action
}
ViewData.Model = viewModel;
return View();
}
The first time I submit the form, the viewData in the target method gets populated correctly.
If I go back and do another search, it's like the modelbinder "cached" the data I submitted the first time: the viewData always has the values from the first search. It resets only restarting the application.
I tried checking both ModelState and HttpContext.Request and they DO have the new data in them (not stale) but still the viewData gets populated with old data. I also tried overriding OnActionExecuting and OnActionExecuted simply to put a breakpoint in there and check the ModelState in those steps, and found nothing weird.
I also tried to call the Search method directly via the browser bar, since it's in GET and I can do it. Still, ModelState and Request contain the data I input, but the viewData has old data.
This is really getting to my nerves as I can't really understand what's going on.
Any help would really be appreciated.

Have you tried
ModelState.Clear()
When you're done with the search call?

I experimented a lot and I found out that the problem was on an actionfilter on a base class which I was unaware of. The "PassParametersDurignRedirect" filter of MvcContrib. Without it everything works fine.

Related

Model values overridden by Request.Form when using EditorFor

I am building a wizard - a series of form steps.
Each step of the wizard are submitted to the same action method (it needs to be highly extendable), and all step viewmodels inherit from the same base model. They are bound to their concrete type by a custom model binder. The submit action saves the form fields and returns the next step (or the same in case of errors).
It works well. However, at one point the user needs to supply work information. The step immediately following primary work is secondary work information - same form, same properties. The viewmodel for the second part inherits from the primary viewmodel without implementing any individual properties (the class definition is empty).
After the primary form is submitted, the secondary viewmodel is returned with values reset. However, for some reason, the form is shown with the first viewmodels values.
And the really strange thing is, the model values are overridden by Request.Form values for all EditorFor calls, but not for specific calls:
// This displays correctly, showing the current model values
<div>Model says: #Model.AuthorizationNumber</div>
<div><input type="text" value="#Model.AuthorizationNumber"/></div>
// This displays the Request.Form value from the previous step.
#Html.EditorFor(x => x.AuthorizationNumber)
In the above, the authorization number is shown correctly for the first two lines (constructor values), but the third is incorrect (showing the Request.Form value).
What could be the problem?
EDIT 1
On request, the controller method handling the submit.
public ActionResult Index(StepPathState model = null)
{
var isPreviousSubmit = Request.Form.AllKeys.Contains("submit-previous");
if (model == null || model.Step == null) return View(Steps.GetFirstPathResult());
// submit the step data
var result = Steps.SubmitStepData(model, isPreviousSubmit);
if (result.Status is StepSuccessAndFinish)
{
return Finish(result);
}
var failureStatus = result.Status as StepActionStatusFailure;
if (failureStatus != null)
{
foreach (var error in failureStatus.Errors)
{
ModelState.AddModelError(error.Property, error.ErrorMessage);
}
}
return View(result);
}
I may have found the answer though: http://blogs.msdn.com/b/simonince/archive/2010/05/05/asp-net-mvc-s-html-helpers-render-the-wrong-value.aspx
Solution: The good and the ugly
As the article linked above describes, it seems that MVC favors the modelstate values when returning from a POST. This is because re-displaying the form in a POST should always indicate a validation failure. So the correct solution is to save, redirect to a GET request and display the new form. This would fix the problem, and is what I am going to do when I have time to refactor.
A temporary solution is to clear the modelstate. This will cause it to fallback back to the model. This is my temporary fix:
if (failureStatus == null && ModelState.IsValid)
{
ModelState.Clear();
}
return View(result);
The danger here is that I have cleared the modelstate. And what might that entail in the long run? New bug adventures to come. But I am on the clock.

Does ASP.NET MVC form post autopopulate fields?

I have a form like ...
#using (Html.BeginForm("create", "Account", FormMethod.Post, new { id = "accountform_form" }))
{
#Html.TextBoxFor(e => e.ShipFirstName)
...
}
while testing, I was surprised to see the field retained its value on postback even without me assigning it to the view-model. Using the debugger, the value for ShipFirstName is null right at the end of the action when returning the view, so why would it show the value that was in the field? Have I been unnecessarily assigning posted values to view-model properties all this time? Or is there something else going on?
Update: the action is like so...
[HttpPost]
public ViewResult Create(AccountFormModel postModel)
{
var model = new AccountFormModel(postModel, stuff, stuff); //I use posted values and paramters to create the actual view model
return view(model);
}
So, I see the GET form, enter values, say I enter a field and leave a required field blank, submit, the resulting page has the value I entered in the other field, who's putting it there when in the model it's null?
I ran into something similar earlier today (a checkbox was always checked). Have a look at Changing ViewModel properties in POST action and see if this is similar.
Basically calling ModelState.Clear() fixed it for me.
As you're passing the model back to the view after it has been POSTed, MVC is taking the stance that you're doing so because the form contains errors. So, rather than making the user fill out the form again, it repopulates it using the ModelState collection. In this case, the values in the ModelState collection take precedence over the changes you make in the action (which does feel a bit weird).
You can get around this either by calling ModelState.Clear() or using ModelState.Remove(string key), where key is the name of the property.
If you'd like a full explanation of why this is the case, see ASP.NET MVC’s Html Helpers Render the Wrong Value!. Excerpt:
Why?
ASP.NET MVC assumes that if you’re rendering a View in response to an HTTP POST, and you’re using the Html Helpers, then you are most likely to be redisplaying a form that has failed validation. Therefore, the Html Helpers actually check in ModelState for the value to display in a field before they look in the Model. This enables them to redisplay erroneous data that was entered by the user, and a matching error message if needed.

How to chage controller's current value provider before TryUpdateModel()

I have a JQuery Ajax call that posts some data (Json) to a MVC Controller. Basically in my method I have
public JsonResult Save()
{
TryUpdateModel(MyModel)
...
}
What I want to do is change some values in the posted Ajax data, before calling TryUpdateModel(). I've tried numerous ways including
ViewData.ModelState.SetModelValue("MyTextBox", new
ValueProviderResult("Some string", string.Empty, new
System.Globalization.CultureInfo("en-US")));
Everything time it appears that the values I'm changing aren't being updated. Any ideas where I'm going wrong ?
Guess I'm still to new to post against your question, but what is your posted object? Model, string, etc. You left some detail out of the code.
Without more detail, this Posting JSON data via jQuery to ASP .NET MVC 4 controller action may help

How does UrlHelper.GenerateContentUrl works?

I dont get it, I have this code:
return JavaScript(string.Format(
"window.location = '{0}'",
UrlHelper.GenerateContentUrl("Index", this.HttpContext)));
The code is inside two pretty generic "Create" methods that works with POST. Each of the two methods are in different controller classes.
Now for method A that is called with the URL http://localhost:56688/Businessrule/Create, when the code is executed I get redirected to http://localhost:56688/Index.
But for method B called from http://localhost:56688/FormulaField/Create I get redirected to http://localhost:56688/FormulaField/Index.
... really I don't get it, and the microsoft documentation isn't helping out much http://msdn.microsoft.com/en-us/library/system.web.mvc.urlhelper.generatecontenturl.aspx (now, IMHO, that's a pretty crappy documentation for a method)
Sounds like your missing the controller name. apperently, you're being redirected to the Index action in the same controller.
It's what the MVC Route engine do, if he does not find the controller name, he assign a default value, in this case, the controller from witch the action has been executed.
Try something like :
UrlHelper.GenerateContentUrl(#"~\ControllerName\Index", this.HttpContext)
So, as pointed by asawyer how it works is answered by the code itself:
https://github.com/aspnet/AspNetWebStack/blob/master/src/System.Web.Mvc/UrlHelper.cs
It turns out that if the string that you pass begin with "~" the method will call PathHelpers.GenerateClientUrl, but if the string doesn't begin with "~" it will just return the same string unchanged.
But still I don't undertand why I'm getting different results. Anyway seems that I'll have to look closer to the raw response passed to the browser...

How Can I Reconstruct URL/Route in ASP.NET MVC From Saved Data?

My unfamiliarity with the ASP.NET MVC framework and the plumbing thereof has brought me here, and I appreciate the patience it will take for anyone to read and consider my question!
Okay, here is the scenario: I have an application that has numerous pages with grids that display data based on searches, drilling down from other data, reports based on context-specific data (i.e. they are on a details page for Foo, then click on a link that shows a table of data related to Foo), etc.
From any and all of these pages, which are all over the app, the user can save the "report" or grid by giving it a name and a description. This doesn't really save the data, displayed in the grid, so much as saves the parameters that define what the grid looks like, saves the parameters that were used to get the data, and saves the parameters that define "where" in the app they are (the action, controller, route) - basically a bunch of metadata about the report/grid and how to construct it.
All of these saved reports are available in a single list, displaying the name and description, on a certain page in the app, with each linking to a generic URL, like "/Reports/Saved/248" (where 248 is an example of the report's ID).
Here is the part I need help on:
When I get to the action via the url "/Reports/Saved/248" and pull the metadata out of the database for that particular report, how can I redirect that data and the request to the same action, controller and route used to display the view that the report was originally saved from? Essentially, I want the user to view the report in the same view, with the same URL as it was saved from. If possible, it would be nice for me to be able to basically "call" that same action as though I am making a method call.
UPDATE: Unfortunately, our report pages (i.e. the pages these grids appear on) are NOT using RESTful URLs - for example, we have what we call an Advanced Search page, which takes a rather large number of potential parameters (nearly 30) that come from a form containing select lists, textboxes, etc. When the user submits that page, we do a POST to an action which accepts a complex type that the model binder builds for us - that same action is what I want to call when the user selects a saved Advanced Search from the database. That example epitomizes my problem.
Thanks
I think that you'll want to use RedirectToAction with the signature that takes a RouteValueDictionary. The method that you are redirecting to will need to be able to pull the values from the ValueProvider on the controller. It might look something like:
public ActionResult Saved( int id )
{
var reportParams = db.Reports.SingleOrDefault( r => r.ID == id );
if (reportParams == null)
...handle error...
var routeValues = ParamsToRouteValueDictionary( reportParams );
return RedirectToAction( reportParams.Action, reportParams.Controller, routeValues );
}
private RouteValueDictionary ParamsToRouteValueDictionary( object parameters )
{
var values = new RouteValueDictionary();
var properties = parameters.GetType().GetProperties()
.Where( p => p.Name != "Action" && p.Name != "Controller" );
foreach (var prop in properties)
{
values.Add( prop.Name, prop.GetValue(parameters,null) );
}
return values;
}
EDIT
Using a filter model as the parameter for your method actually may make it easier. You just need GET and POST versions of your action.
[ActionName("People")]
[AcceptVerbs( HttpVerbs.Get )]
public ActionResult PeopleDisplay( SearchModel filter )
{
return People( filter );
}
[AcceptVerbs( HttpVerbs.Post)]
[ValidateAntiForgeryToken]
public ActionResult People( SearchModel filter )
{
....
}
Then you would store in your db for the report the filter parameters (by name), the Action ("People"), and the Controller. The redirect result will use GET and be directed to the PeopleDisplay method, which in turns simply calls the People method with the correct parameter. Posting from the form calls the People method directly. Using two methods allows you to use the CSRF prevention mechanism. You might be able to use a flag in TempData to ensure that the GET action is only invoked via the redirection mechanism if you care to restrict access to it.
END EDIT
Another alternative, would be to simply store the View used as well and instead of doing a redirect, just render the appropriate view. One of the things that you'll want to consider is that doing the redirect will end up with a URL containing all the parameters, whereas rendering the View will leave the URL alone and just display the same view as the URL used when creating the report.
You can use the RedirectToAction method to issue a 301 redirect to a specific action method on any controller, along with route values:
ReportMeta meta = _reportDataAccess.Get(id);
return RedirectToAction(meta.Action, meta.Controller, meta.RouteData);
where those values are something like:
meta.Action = "Bar";
meta.Controller = "Foo";
meta.RouteData = new {
// possibly settings for the grid
start = DateTime.Min,
end = DateTime.Now,
sort = "Date"
// you get the idea
};
Of course, the immediate issue I can see with this is what happens when your controller/action methods change over time, the report data will be invalid. But then you probably thought of that already.

Categories

Resources