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.
Related
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!
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)
Controller:
OnePersonAllInfoViewModel vModel = new OnePersonAllInfoViewModel();
vModel.PreferredContactType = new PreferredContactType();
ViewBag.PrefContactTypes = new SelectList(dbEntities.PreferredContactTypes
.OrderBy(pct => pct.PreferredContactTypeID),
"PreferredContactTypeID", "PreferredContactType1",
vModel.PreferredContactType.PreferredContactTypeID);
View:
<div class="editor-label">
#Html.LabelFor(model => model.PreferredContactType.PreferredContactTypex)
</div>
#Html.DropDownListFor(model => model.PreferredContactType.PreferredContactTypeID,
ViewBag.PrefContactTypes as SelectList)
And I get this error on post back... There is no ViewData item of type 'IEnumerable' that has the key 'PreferredContactType.PreferredContactTypeID'
Any thoughts? Thanks!
In your HttpPost controller action you must repopulate the ViewBag.PrefContactTypes property the same way you did in your GET action if you redisplay the same view:
[HttpPost]
public ActionResult Process(OnePersonAllInfoViewModel model)
{
ViewBag.PrefContactTypes = ...
return View(model);
}
Also you seem to have defined some class that is suffixed with ViewModel. This leaves the reader to believe that you are using view models in your application and in the very next line you use ViewBag. Why? Why not take full advantage of the view model and its strong typing?
Just like this:
public class OnePersonAllInfoViewModel
{
public int PreferredContactTypeID { get; set; }
public IEnumerable<PreferredContactType> PrefContactTypes { get; set; }
}
and then in your GET action:
public ActionResult Index()
{
var model = new OnePersonAllInfoViewModel();
model.PrefContactTypes = dbEntities
.PreferredContactTypes
.OrderBy(pct => pct.PreferredContactTypeID)
.ToList();
return View(model);
}
then the view:
#Html.DropDownListFor(
model => model.PreferredContactTypeID,
Model.PrefContactTypes
)
and the POST action:
[HttpPost]
public ActionResult Index(OnePersonAllInfoViewModel model)
{
if (!ModelState.IsValid)
{
// the model is invalid => we must redisplay the same view =>
// ensure that the PrefContactTypes property is populated
model.PrefContactTypes = dbEntities
.PreferredContactTypes
.OrderBy(pct => pct.PreferredContactTypeID)
.ToList();
return View(model);
}
// the model is valid => use the model.PreferredContactTypeID to do some
// processing and redirect
...
// Obviously if you need to stay on the same view then you must ensure that
// you have populated the PrefContactTypes property of your view model because
// the view requires it in order to successfully render the dropdown list.
// In this case you could simply move the code that populates this property
// outside of the if statement that tests the validity of the model
return RedirectToAction("Success");
}
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.
Okay i'm new to asp mvc2 and i'm experiencing some problems with the
htmlhelper called Html.dropdownlistfor();
I want to present the user a list of days in the week. And i want the selected item to be bound to my model.
I have created this little class to generate a list of days + a short notation which i will use to store it in the database.
public static class days
{
public static List<Day> getDayList()
{
List<Day> daylist = new List<Day>();
daylist.Add(new Day("Monday", "MO"));
daylist.Add(new Day("Tuesday", "TU"));
// I left the other days out
return daylist;
}
public class Dag{
public string DayName{ get; set; }
public string DayShortName { get; set; }
public Dag(string name, string shortname)
{
this.DayName= name;
this.DayShortName = shortname;
}
}
}
I really have now idea if this is the correct way to do it
Then i putted this in my controller:
SelectList _list = new SelectList(Days.getDayList(), "DayShortName", "DayName");
ViewData["days"] = _list;
return View("");
I have this line in my model
public string ChosenDay { get; set; }
And this in my view to display the list:
<div class="editor-field">
<%: Html.DropDownListFor(model => model.ChosenDay, ViewData["days"] as SelectList, "--choose Day--")%>
</div>
Now this all works perfect. On the first visit, But then when i'm doing a [HttpPost]
Which looks like the following:
[HttpPost]
public ActionResult Registreer(EventRegistreerViewModel model)
{
// I removed some unrelated code here
// The code below executes when modelstate.isvalid == false
SelectList _list = new SelectList(Days.getDayList(), "DayShortName", "DayName");
ViewData["days"] = _list;
return View(model);
}
Then i will have the following exception thrown:
The ViewData item that has the key 'ChosenDay' is of type 'System.String' but must be of type 'IEnumerable<SelectListItem>'.
This errors gets thrown at the line in my view where i display the dropdown list.
I really have no idea how to solve this and tried several solutions i found online. but none of them really worked.
Ty in advance!
I have seen such an error.
This is becouse ViewData["days"] as SelectList is null when the view is rendered. This can be becouse of ViewData["days"] is null or in different type then SelectList.
The problem must be found here:
[HttpPost]
public ActionResult Registreer(EventRegistreerViewModel model)
{
}
Make shure, that this code
SelectList _list = new SelectList(Days.getDayList(), "DayShortName", "DayName");
ViewData["days"] = _list;
runs and that ViewData["days"] not null and IEnumerable before you return View. It must be becouse of Model.IsValid so ViewData["days"] not binded.
The HttpPost controller action will call the model constructor when it sees "EventRegistreerViewModel model".
[HttpPost]
public ActionResult Registreer(EventRegistreerViewModel model)
So if you add code to your EventRegistreerViewModel model like this:
...
public IEnumerable<string> MySelectList { get; set; }
public EventRegistreerViewModel() {
// build the select the list as selectList, then
this.MySelectList = selectList;
}
Then in your view use this code to display the list and the selected item:
Html.DropDownListFor(model => model.ChosenDay, model.MySelectList, "--choose Day--")
This way every time the view model is constructed it will include the select list