ASP.NET Core MVC Model Binding Bug? - c#

I'm having some trouble with ASP.NET Core's model binding. Basically I'm just trying to bind some POSTed json to an object model with nested properties. Minimal code is provided below for a single button that, when pressed, will POST to a Controller action method via XMLHttpRequest. The action method takes a single model class parameter with the [FromBody] attribute.
Model:
public class OuterModel {
public string OuterString { get; set; }
public int OuterInt { get; set; }
public InnerModel Inner { get; set; }
}
public class InnerModel {
public string InnerString { get; set; }
public int InnerInt { get; set; }
}
Controller:
using Microsoft.AspNetCore.Mvc;
public class HomeController : Controller {
[HttpPost("models/")]
public IActionResult Save([FromBody] OuterModel viewModel) {
if (!ModelState.IsValid)
return BadRequest(ModelState);
// Return an appropriate response
return Ok();
}
}
Razor markup for a "submit" button:
<div class="form-row">
<div class="col-2">
#{ string url = Url.Action(nameof(HomeController.Save), "Home"); }
<button id="post-btn" data-post-url="#url">POST</button>
</div>
</div>
JavaScript to submit (DOES NOT bind):
document.getElementById("post-btn").addEventListener("click", e => {
const xhr = new XMLHttpRequest();
xhr.addEventListener("timeout", () => console.error("Timeout"));
xhr.addEventListener("error", () => console.error("Error"));
xhr.addEventListener("load", () => console.log(`Status: ${xhr.status} ${xhr.statusText}`));
xhr.open("POST", e.target.dataset.postUrl);
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhr.send(JSON.stringify({
"OuterString": "outer",
"OuterInt": 1,
"Inner.InnerString": "inner",
"Inner.InnerInt": 5
}));
});
Looking at that JavaScript, I would expect the Inner.* json property names to bind correctly, given this line from the docs:
Model binding looks for the pattern parameter_name.property_name to bind values to properties. If it doesn't find matching values of this form, it will attempt to bind using just the property name.
But it doesn't; the OuterModel.Inner property in the action method ends up null. The following json does bind correctly, however:
JavaScript to submit (DOES bind):
xhr.send(JSON.stringify({
"OuterString": "outer",
"OuterInt": 1,
"Inner": {
"InnerString": "inner",
"InnerInt": 5
}
}));
So I can use this code to achieve the model binding that I need, I'm just confused as to why the first JavaScript didn't work. Was I not using the correct naming convention for nested properties? Some clarification would be much appreciated!

Related

MVC actionlink passing list of objects

I've got a view that looks like this:
With this view I'm filtering records which are in a database. Each "filter" is a SearchViewModel which has a class definition like this:
public class SearchViewModel
{
//Property has a property called "SqlColumnName"
public Property Property { get; set; }
public Enums.SearchOperator Operator { get; set; }
public string Value { get; set; }
}
I'm now trying to find a solution how to build a actionlink to this site and passing in a List<SearchViewModel>().
So I'm trying to accomplish something like this:
http://url/Index?property=Typ&Operator=2&Value=4&property=ServerName&Operator=1&Value=server
I've tried to solve my problem with this http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/ but this guy is calling the Action with a POST and i need to call it with a GET request.
So how to pass a list of objects to a mvc controller action with an actionlink?
EDIT
I think i need to say, that POST request is not an option because i need that link in an onClick event of a div element.
You could construct a link with the following parameters that should work:
http://url/Index?%5B0%5D0.property=Typ&%5B0%5D0.Operator=2&%5B0%5D0.Value=4&%5B1%5D0.property=ServerName&%5B1%5D0.Operator=1&%5B1%5D0.Value=server
Also notice that there are restrictions in the length of query string parameters. Those restrictions vary between the browsers. So it's probably a better approach to simply generate a link with an id that would allow you to retrieve the corresponding collection on the server (from your datastore):
http://url/Index?id=123
ActionLink helper method renders an anchor tag. It is ok to pass few query string items via a link. Remember Query string has a limit about how much data you can pass and it varies from browser to browser.
What you should be doing is a form posting. You can do a form posting on a click event on a div with the help of a little javascript.
Let's create a new view model for our search page
public class SearchVm
{
public List<SelectListItem> Operators { set; get; }
public List<SearchViewModel> Filters { set; get; }
}
public class SearchViewModel
{
//Property has a property called "SqlColumnName"
public Property Property { get; set; }
public SearchOperator Operator { get; set; }
public string Value { get; set; }
}
So in your GET action, You will send a list of SearchViewModel to the view.
public ActionResult Index()
{
var search = new SearchVm
{
Filters = new List<SearchViewModel>
{
new SearchViewModel {Property = new Property {SqlColumn = "Name"}},
new SearchViewModel {Property = new Property {SqlColumn = "Age"}},
new SearchViewModel {Property = new Property {SqlColumn = "Location"}}
}
};
//Convert the Enums to a List of SelectListItem
search.Operators= Enum.GetValues(typeof(SearchOperator)).Cast<SearchOperator>()
.Select(v => new SelectListItem
{
Text = v.ToString(),
Value = ((int)v).ToString()
}).ToList();
return View(search);
}
And in your view, which is strongly typed to your SearchVm view model, We will manipulate the form field names so that the model binding will work when the form is submitted.
#model SearchVm
#using (Html.BeginForm())
{
var i = 0;
foreach (var criteria in Model.Filters)
{
<label>#criteria.Property.SqlColumn</label>
#Html.HiddenFor(f => f.Filters[i].Property.SqlColumn)
#Html.DropDownList("Filters[" + i+ "].Operator",Model.Operators)
#Html.TextBoxFor(f=>f.Filters[i].Value)
i++;
}
<div id="someDiv">Search button using Div</div>
<input type="submit" value="Search" />
}
And your HttpPost action method to handle the form submit.
[HttpPost]
public ActionResult Index(SearchVm model)
{
foreach(var f in model.Filters)
{
//check f.Property.SqlColumn, f.Value & f.Operator
}
// to do :Return something useful
}
If you want the form to be submitted on the click event on the div, Listen to the click event on the specific div and call the submit method on the form the div resides in.
<script>
$(function () {
$("#someDiv").click(function(e) {
$(this).closest("form").submit();
});
});
</script>

Keep a value in a ViewModel even if the value is not modified

In a C# MVC 5 Internet application, I have a HTTP Get Edit action result, that gets an object, and places this object in a ViewModel and this is then displayed in a View.
One of the fields in the ViewModel is a value that is not edited in the view. In the HTTP Post Edit action, the value that is not edited in the view has been reset.
How can I keep this value so that it is the same value in the HTTP Post method as the HTTP Get method?
Thanks in advance
EDIT
Here is the ViewModel code:
public class MapLocationViewModel
{
[Editable(false)]
public int mapCompanyForeignKeyId { get; set; }
public MapLocation mapLocation { get; set; }
}
Here is the code at the bottom of the HTTP Get Edit Action result, where the mapCompanyForeignKeyId is set:
MapLocationViewModel mapLocationViewModel = new MapLocationViewModel();
mapLocationViewModel.mapLocation = maplocation;
mapLocationViewModel.mapCompanyForeignKeyId = maplocation.mapCompanyForeignKeyId;
return View(mapLocationViewModel);
Here is the HTTP Post Edit Action result code:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit(MapLocationViewModel mapLocationViewModel)
In the above HTTP Edit Action result code, the mapLocationViewModel.mapCompanyForeignKeyId is reset to 0, after this value has been set to a number in the HTTP Get Edit Action result.
You should try make hidden input. With Razor syntax it would be:
#using (Html.BeginForm())
{
#Html.HiddenFor(model => model.YourProperty)
}
YourProperty will not be visible but it's value will be in the view model sent to the POST method.
You can also use HiddenInputAttribute for this:
[HiddenInput(DisplayValue=false)]
public int YourProperty {get; set;}
If you are using the #Html.TextboxFor(m => m.MyField) or similar helpers within a form, by default, it should automatically spit out all the existing values for each field and thus you should see all values whether modified or not. When it is posted, each included field will be serialized. If you use the helpers, you won't have to worry about naming convention as Razor and the model binder will do the work for you.
Check the request coming into your POST action to see if it is a model binding issue or a client issue. If you don't see the desired members in the body (or query string, if a GET) then you must not be sending them from the client, which can be due to improper serialization/naming of fields, not including the field in the page, not sending the value of the field to the page, or including the field outside of the form, among other reasons...
Example:
public class MyViewModel
{
[Required]
public string Field1 { get; set; }
[Required]
public string Field2 { get; set; }
}
...
#model MyViewModel
#using (Html.BeginForm("MyAction", ...)
{
#Html.LabelFor(m => m.Field1)
#Html.TextboxFor(m => m.Field1)
<br />
#Html.LabelFor(m => m.Field2)
#Html.TextAreaFor(m => m.Field2)
<button type="submit">Submit</button>
}
...
[HttpPost]
public ActionResult MyAction(MyViewModel model)
{
if (!ModelState.IsValid)
return MyGetAction(model);
...
}

Can't make BindAttribute Prefix Property to work in ASP.NET MVC3

I am unable to make a simple MVC 3 controller/view/model program work with an ActionResult method that includes the Bind attribute with a Prefix property.
One example that I did try could populated the parameter when I call the action method from the URL.
Here is that example controller followed by its view:
//Controller
//public ActionResult PrefixExample(int number = 0)
public ActionResult PrefixExample([Bind(Prefix="okay")]int? number)
{
return View(number);
}
//View
#model Int32?
#{
ViewBag.Title = "Example";
}
<h2>Example</h2>
#using (Html.BeginForm())
{
if (#Model.HasValue)
{
<label>#Model.Value.ToString()</label>
} else {
<label>#Model.HasValue.ToString()</label>
}
<input type="submit" value="submit" />
}
If I use this url http://localhost/MVCApp/Home/Example?okay=3 the parameter, number, is populated. If I use this url http://localhost/MVCApp/Home/Example?number=3, the parameter isn't populated. Interestingly, with the first url, when I view source, the prefix okay doesn't show up.
If I uncomment the first line of my controller and comment out the second line, the opposite is true: the url with okay won't populate number but the second url using number will populate number in the controller.
I would like to know how to make the following example accept a url and correctly set the "view source" prefix. Here is a possible url http://localhost/MVCApp/Home/SpecificPerson?PersonId=0&FirstName=Joe&LastName=Doe
Note, that if I remove the Bind attribute from the controller method, the above url will work with the MVC app below.
Here is my model/controller/view:
//model:
namespace MVCApp.Models
{
public class Person
{
[HiddenInput(DisplayValue = false)]
public int PersonId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
}
//controller
namespace MVCApp.Controllers
{
public class HomeController : Controller
{
public ActionResult SpecificPerson([Bind(Prefix = "myPerson")]Person aPerson)
{
return View("SpecificPerson", aPerson);
}
}
}
//view
#model MVCApp.Models.Person
#{
ViewBag.Title = "SpecificPerson";
}
<h2>SpecificPerson</h2>
#Html.EditorForModel();
<br />
#Html.EditorFor(m => m);
I would like to see the above example work. Anyone who could show me why it doesn't work as I expect or what I can do to make it work this way would be greatly appreciated.
Thank you in advance.
I think the EditorForModel brought you a bit off track. If you check the html that is generated by this helper you will see that it's not wrapped in a form. Besides that I think the EditorForModel will not serve you as much as you would like to. You can also get it to work correctly without specifying the Bind prefix.
//model
public class Person
{
public int Id {get;set;}
public string FirstName {get;set;}
public string LastName {get;set;}
}
//controller
public class HomeController : Controller
{
public ActionResult Index(Person person)
{
if("POST".Equals(Request.HttpMethod))
{
//for example do some validation and redirect
}
return View(person);
}
}
//view
#model Application.Models.Person //only as example use own
#using(Html.BeginForm("Index","Home", FormMethod.POST))
{
#Html.HiddenFor(x=> x.Id)
<div>
#Html.LabelFor(x=> x.FirstName)
#Html.TextBoxFor(x=> x.FirstName)
</div>
<div>
#Html.LabelFor(x=> x.LastName)
#Html.TextBoxFor(x=> x.LastName)
</div>
<input type="submit" value="Do a post request"/>
}
Also if you use a url like /Home/Index?Id=9 and you look the HTML code you will see that there will be a element with input type=hidden and the value of 9. You could also use two actionresults to split your logic with [HttpGet] and [HttpPost] as attribute of your Action.
And as last I recommend you to check out the newer versions of MVC; MVC 5 is already out...

Do I need a custom modelbinder for my Create Action?

I have been doing a bit of research on this but, I am having a little trouble understanding when modelbinding is needed in MVC 3. I have created a ViewModel to supply data to my Create view.
public class InvitationRequestViewModel
{
public InvitationRequest InvitationRequest { get; private set; }
public IEnumerable<SelectListItem> EventsList { get; private set; }
public string EventId { get; set; }
public InvitationRequestViewModel(InvitationRequest request)
{
InvitationRequest = request;
EventsList = new SelectList(MyRepositoryAndFactory.Instance.FindAllEvents()
.Select(events => new SelectListItem
{
Value = events.ID.ToString(),
Text = String.Format("{0} - {1} - {2}", events.Name, events.Location, events.StartDate.ToShortDateString())
}
), "Value", "Text");
}
}
My InvitationRequest controller has the following Action methods
public ActionResult Create()
{
InvitationRequest request = new InvitationRequest(User.Identity.Name);
return View(new InvitationRequestViewModel(request));
}
[HttpPost]
public ActionResult Create(InvitationRequestViewModel newInvitationRequest)
{
try
{
if (!ModelState.IsValid) return View(newInvitationRequest);
newInvitationRequest.InvitationRequest.Save();
MyRepositoryAndFactory.Instance.CommitTransaction();
return RedirectToAction("Index");
}
catch
{
ModelState.AddModelError("","Invitation Request could not be created");
}
return View(newInvitationRequest);
}
I can reach the Create view with no problems and the DDL is populated with a list of available events. My problem is that I was expecting the InvitationRequestViewModel to be mapped to the HttpPost Create method. Instead, I just get an error saying "The website cannot display the page".
When I use the signature:
public ActionResult Create(FormCollection collection){ }
then I can see the posted values. I had hoped not to have to do my own mapping code in the controller.
Is a custom ModelBinder the answer?
EDIT
I am using a strongly typed View of type InvitationRequestViewModel and this is the DDL code
<div class="editor-label">
#Html.LabelFor(model => model.InvitationRequest.Event)
</div>
<div class="editor-field">
#Html.DropDownListFor(x => x.EventId, Model.EventsList)
</div>
You have to specify a parameterless constructor for the InvitationRequestViewModel so the default model binder can instantiate it.

Default ModelBinder not working properly

I have this following structure:
public class Dummy
{
public string Name { get; set; }
public InnerDummy Dum { get; set; }
}
public class InnerDummy
{
public string Name { get; set; }
}
And an ActionResult that receives a Dummy
[HttpPost]
public ActionResult Index(Dummy dum)
{
var dsad = dum;
//var dwss = idum;
return RedirectToAction("index");
}
On my view I have:
#model TestMVC3Razor.Controllers.HomeController.Dummy
#using (Html.BeginForm())
{
#Html.TextBoxFor(o => o.Name)
#Html.EditorFor(o => o.Dum)
<br />
<br />
<input type="submit" />
}
It is posting
Name=xxx
Dum.Name=yyy
But when I try to get dum.Dum.Name on the ActionResult I get null instead of yyy. Is this a bug or just the way it is? Am I not using it right? Do I need to implement a new binder for this?
HI~ your view should use #Html.EditorFor(o => o.Dum.Name)
not #Html.EditorFor(o => o.Dum)
And postback Controller:
[HttpPost]
public ActionResult Index(Dummy postback)
{
var dsad = postback;
var a = postback.Name; //get Name of Dummy
var b = postback.Dum.Name; //get Name of InnerDummy of Dummy
return RedirectToAction("index");
}
If you have problems about it, please let me know :)
You need to pull the InnerDummy out from inside the Dummy class.
When the default model binder finds the Dum property it will try to create an object of type InnerDummy, but in its context that doesn't exist. To reference InnerDummy as you have it the model binder would need to create a Dummy.InnerDummy, but it has no way of knowing that.
Making InnerDummy a direct member of the namespace will fix the problem.
It may also be possible to fix the problem by declaring Dum as:
public Dummy.InnerDummy Dum { get; set; }
I'm not sure about this, though.

Categories