Default ModelBinder not working properly - c#

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.

Related

ASP.NET Core MVC Model Binding Bug?

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!

How to add code to an MVC cshtml file view

really new to MVC and not sure how i would get my variable 'UsersFullName' from the code behind class file to the view. With aspx and aspx.cs i would have done this:
index.aspx.cs:
using System.DirectoryServices.AccountManagement;
namespace TeamRequestForm.Controllers
{
public class AddTeamController : Controller
{
// GET: AddTeam
public ActionResult Index()
{
return View();
}
private string usersFullName = UserPrincipal.Current.DisplayName;
public string UsersFullName { get { return usersFullName; } }
}
}
index.aspx:
<input type="text" value="<%=UsersFullName%>" name="changeOriginator" id="changeOriginator">
How can I do the same in a view? Many thanks in advance.
Create a Model:
public class UserModel
{
public string FullName { get; set; }
// Any other properties
}
And return it to your view:
public ActionResult Index()
{
var viewModel = new UserModel { FullName = // Logic for set };
return View(viewModel);
}
And then in your view, declare this at the top of your view:
#model namespace.UserModel
And in the input you can reference this model by using #model.FullName or, even better:
#Html.EditorFor(model => model.FullName)
Viewbag is one way to do it. Adding it to your model is another. There are more options. Have a look at these articles
http://www.c-sharpcorner.com/uploadfile/abhikumarvatsa/various-ways-to-pass-data-from-controller-to-view-in-mvc/
http://www.webdevelopmenthelp.net/2014/06/asp-net-mvc-pass-data-controller-view.html
public ActionResult Index()
{
ViewBag.UsersFullName = UsersFullName;
return View();
}
View
#Html.TextBoxFor(c => c.UsersFullName )
check out the MVC documentation on StackOverflow
You can do this from many ways, but these are most popular approaches:
1) using ViewBag
public ActionResult Index()
{
private string usersFullName = UserPrincipal.Current.DisplayName;
ViewBag.userFullName=usersFullName;
return View();
}
and call this ViewBag at your .cshtml page
like below:
<span>#ViewBag.userFullName</span>
2) using ModelView
For ViewModel approach you have to create Properties for your Entities in class like below:
public class UserViewModel
{
public string userFullName{get; set;}
}
and use this ViewModel into your Controller like this
public ActionResult Index()
{
private string usersFullName = UserPrincipal.Current.DisplayName;
UserViewModel objCls=new UserViewModel();
objCls.userFullName=usersFullName;
return View(objCls);
}
and use on .cshtml page like below:
#model UserViewModel //your class refrence
<span>#Model.userFullName</span> // you will get your value here
Hope it will helps you. Thanks

Is my view model too "complex" to allow binding on post?

Using my model displaying a page works fine but the post does not return the bound model.
My classes:
public class ContactManager
{
public Contact Contact { get; set; }
public SelectList SalutationList { get; set; }
}
public class Contact
{
public int Id{get;set;}
public string FirstName{get; set;}
public SalutationType SalutationType{get; set;}
}
public class SalutationType
{
public int Id { get; set; }
public string Name { get; set; }
}
My View:
#model ViewModels.ContactManager
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
#Html.HiddenFor(model => model.Contact.Id)
#Html.DropDownListFor(model => model.Contact.SalutationType.Id, Model.SalutationList)
#Html.EditorFor(model => model.Contact.FirstName)
<input type="submit" value="Save" />
}
The issue seems to be in the DropDownListFor. The dropdown list displays correctly with the proper value but when I post this page the complete model is blank. If I simplify the DropDownListFor like this the values are posted as expected.
#html.DroDownListFor(model=>model.MyPlaceHolderProp, Model.SalutationList)
Is my model too complex? Am I not doing something correctly?
The models are based off of several tables using EF that I have created in a separate project. I am trying to avoid creating more classes/models then I have to.
You should post your controller action as well, as your model coming back as blank really has nothing to do with this. Changing the DropDownListFor definition one way or another should not effect the posting of any other values.
That said, you will run into another issue eventually here, so you need to regroup, anyways. You can't just post back the id value of a related item. Entity Framework will either complain that there's already an object with that id, or worse, if the object attaches, it will update the row with that id with the new posted value for Name, which in this case, is nothing, so it'll just clear it out.
When you create a relationship with a single item (a foreign key basically), if you don't specify a property to hold that foreign key value, Entity Framework creates one for you behind the scenes to track the relationship. In your case here, that means your Contacts table has a column named SalutationType_Id. However, there's no way from your class to directly access this value. This is why I recommend that you always provide an explicit property to handle the relationship:
[ForeignKey("SalutationType")]
public int SalutationTypeId { get; set; }
public SalutationType SalutationType { get; set; }
If you do that, then you can directly stuff the posted id there and Entity Framework will create the relationship.
#Html.DropDownListFor(m => m.Contact.SalutationTypeId, Model.SalutationList);
If you insist on keeping the key implicit, then you must create the relationship yourself, by creating a field on your view model to hold the posted value, then using that value to look up the SalutationType instance from the database, and then finally adding that to the Contact instance.
Add to your view model
public int SalutationTypeId { get; set; }
In your view
#Html.DropDownListFor(m => m.SalutationTypeId, Model.SalutationList)
In your POST action
var salutationType = db.SalutationTypes.Find(model.SalutationTypeId);
contact.SalutationType = salutationType;
You could do it this way. This may be the more "MVC best practice" way to handle it. Everything stays neatly in their models, and no manual IDs are required. The views are intended to be representations of the underlying models they are built on. If you are creating a view that has a form, then create a model that represents the form and use it in the view.
Revise your models like:
public class PostModel
{
public int ContactID { get; set; }
public int SalutationID { get; set; }
public string FirstName { get; set; }
}
public class PostView
{
public ContactManager contact { get; set; }
public PostModel post { get; set; }
}
Then create the PostView in the controller:
public ActionResult Index()
{
//create the PostView model
var pv = new PostView();
pv.ContactManager = contactManager;
pv.post = new PostView()
{
ContactID = contactManager.Contact.Id,
SalutationID = contactManager.SalutationType.Id,
FirstName = contactManager.Contact.FirstName
};
return View(pv);
}
Then the view could be like:
#model ViewModels.PostView
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
#Html.HiddenFor(model => model.post.ContactID)
#Html.DropDownListFor(model => model.post.SalutationID, model.contact.SalutationList)
#Html.EditorFor(model => model.post.FirstName)
<input type="submit" value="Save" />
}
Then the post action in the controller:
[HttpPost]
public ActionResult Index(PostView pv)
{
//post code
//the posted data will be in pv.post
}
Have you considered using a custom model binder? Custom model binding isn't all that complicated for models that are still relatively simple, and you can handle the serialization/deserialization however you need to.
http://msdn.microsoft.com/en-us/magazine/hh781022.aspx
http://ivonna.biz/blog/2012/2/2/custom-aspnet-model-binders-series,-part-3-subclassing-your-models.aspx
http://forums.asp.net/t/1944696.aspx?what+is+custom+model+binding+in+mvc
I am not sure this will help you... I wsa having a similar issue but I was using ajax to post back... anyway, I had forgotten to mark my binding class with the [Serializable] attribute.
so you might try
[Serializable]
public class Contract {
...
}
Again, I am using Json to post back to my controller so may not be related or help you. But, I guess could be worth a try.

Do you always have to set ViewBag variables in order to access data from the controller

I have a controller like this:
public ActionResult Index()
{
ViewBag.Title = "Index";
ViewBag.LoggedIn = TheUser.CheckStatus();
return View();
}
Thing is, I have to set LoggedIn to the output of my other function TheUser.CheckStatus() so that I can reference it with razor... Is there a way in Razor to access a function straight off? for example...
#TheUser.CheckStatus
instead of
#ViewBag.LoggedIn
The recommended way in MVC for passing information to a view is to create a model specific to that view (aka view model) e.g.
public class IndexViewModel
{
public string Title { get; set; }
public bool IsAuthenticated { get; set; }
}
....
public ActionResult Index()
{
return View(new IndexViewModel()
{
Title = "Index",
IsAuthenticated = UserIsLoggedIn()
});
}
However, to answer your question:
Is there a way in Razor to access a function straight off?
If you are using ASP.NET Membership you can use the IsAuthenticated property on the request e.g.
#Request.IsAuthenticated
Otherwise, you do need to pass this information to the view (whether that be via ViewBag/view model etc.)
Alternatively, you could write your own extension method for Request which would allow you to access it directly in the view:
#Request.UserLoggedIn()
Or even as a HtmlHelper e.g.
public static class HtmlHelperExtensions
{
public static bool UserIsLoggedIn(this HtmlHelper helper)
{
return /* authentication code here */
}
}
Then in your views you can use #Html.UserIsLoggedIn() which I think is what you are after.
use a ViewModel class (your view will then be strongly typed, and you'll be able to use "classic" helpers).
//viewModel class
public class UserStatusViewModel {
public string Title {get;set;}
public bool IsLogged {get;set;
}
//action
public ActionResult Index() {
var model = new UserStatusViewModel{ Title = "Index", IsLogged = TheUser.CheckStatus()};
return View(model);
}
//view
#model UserStatusViewModel
#Html.DisplayFor(m => m.Title)
#Html.DisplayFor(m => m.IsLoggedIn)

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.

Categories