I am trying to build a object/model in memory that is a representation of what the model would look like if the current ModelState was assumed to be correct and was Merged into the model.
I thought I would look into the source code for TryUpdateModel() or UpdateModel() and came up with the following:
private T GetTempModel<T>(T model, ActionExecutedContext filterContext)
{
var mtype = Type.GetType(model.GetType().FullName);
var m = Activator.CreateInstance(mtype);
var binder = ModelBinders.Binders.GetBinder(mtype);
var bc = new ModelBindingContext()
{
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => m, mtype),
ModelName = null,
ModelState = filterContext.Controller.ViewData.ModelState,
ValueProvider = filterContext.Controller.ValueProvider
};
var result = binder.BindModel(filterContext.Controller.ControllerContext, bc);
return (T)result;
}
However when I do this the result value is just the default state that was created when I ran the CreateInstance(). It didn't merge all the proposed values in the ModelState.
Am I misunderstanding how this should work? Is there a way to get a model representation of the items in ModelState in object form instead of KeyValuePair?
UPDATE: Additional information per comments.
I am simplifying this a bit but here is the basic scenario for what I am doing. Let's say I have a survey application. The user logs in and is presented with a basic View where they enter their profile information (first, last name etc.) The viewModel used to generate this view has a property, SurveyQuestions, that is initialized with no questions. As they navigate through the next screens they are prompted with questions, these questions are loaded into the View/DOM via AJAX for performance reasons as they answer one question I go grab the next applicable one.
When the user is complete and POSTs the survey I get everything as expected in the ViewModel and in SurveyQuestions I can see all their questions/answer information (List<QuestionBlock>).
However I am using the Post-Redirect-Get (PRG) pattern and if there is an exception during the POST I store the ModelState, redirect, and reapply the ModelState upon exiting the GET. The problem is that in the GET the ViewModel is just the uninitialized and empty question SurveyQuestions property. Because of this the ModelState values aren't initialized properly for the SurveyQuestions when the view is rendered. All other properties get the correct ModelState values when the view is rendered.
This is because I have to initialize the user's SurveyQuestions to have each of the question information before exiting the GET.
This is where I would like to read the values from the ModelState into a temporary object so that I can access them and rebuild those questions server side to correctly rendered in the view.
Related
From my controller I send a ViewModel with a collection of strings to be used in a <select> tag like so...
Controller:
var model = new InviteViewModel
{
SelectItems = new SelectViewModel
{
Companies = _companyRepository.GetCompanyNames()
}
};
Razor View:
<select class="form-control company_select" asp-for="Company" asp-items="#(new SelectList(Model.SelectItems.Companies))"></select>
This works perfectly and will display all the items in the drop down box. However when I go to submit the form the Companies object will be null and when the View is sent back I get a null reference exception. Normally I would create a hidden <input> tag to hold the value, but how can I do this with a collection?
There are so many ways to do this.
It seems like you may have a fundamental misunderstanding of the disconnect between Razor and html. Razor executes server side, and its result is simply a string that gets written to the response stream. Once written, razor's scope is gone and cannot hold data.
One option would be to store the collection in the application cache or session cache with a guid as the dictionary key, and then use a hidden input for the cache key. When the view is being recreated you would then have access to the server and could gather the collection.
This makes the assumption that the collection hasn't changed during the time the view was active, which given some user habits could have been a long time. There should also be some sort of metrics used when caching to invalidate old data, if this is the route you take.
Another option is to simply regenerate the collection from wherever (database?) it came from.
Lastly, you mention that the view is being returned with the empty collection, are you returning a view from a post method? That is bad practice. Look up the "post-redirect-get pattern" for why and how to avoid it.
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.
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.
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.
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.