I have an extended User class like this:
public class User : IdentityUser
{
public string Nombre { get; set; }
public string Apellidos { get; set; }
public int DepartamentoID { get; set; }
public Departamento Departamento { get; set; }
}
In my Edit view I have this field definition:
<div class="form-group">
#Html.LabelFor(model => model.Roles.FirstOrDefault().RoleId, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownListFor(m => m.Roles.ElementAtOrDefault(0).RoleId, (SelectList)ViewBag.RoleList, "Seleccionar un rol", new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.Roles.FirstOrDefault().RoleId)
</div>
</div>
When I send the form, the Roles collection is empty.
Why the Binder does not add the role to the Roles collection?
Greetings and thanks.
I try to add more information to respond to Rajesh's comments.
In the Get action the model contains the information of the role, and the view shows it correctly. A drop-down list shows the available roles, and the user's role appears selected. When in the view I select another different role and send the form, in the Post action, the Roles collection of the model no longer contains information.
GET action
POST action
I do not know how to debug the work of the Binder
Why the Binder does not add the role to the Roles collection?
It happens because #Html.DropDownListFor and default model binder are not smart enough. Your #Html.DropDownListFor produces something like this:
<select class="form-control" id="RoleId" name="RoleId">
<option value="1">Role_1</option>
<option value="2">Role_2</option>
</select>
Since name=RoleId the model binder will try to bind it to RoleId property of your model and it knows nothing about Roles property and moreover that Roles prop is enumerable.
To make it work your model must have RoleId property or you can use Html.ListBoxFor extension if you want to select multiple roles:
#Html.ListBoxFor(m => m.SelectedRoles, (SelectList)ViewBag.RoleList, new { #class = "form-control" })
Then your model must have public List<string> SelectedRoles { get; set; } property.
Another option is to create a custom model binder using IModelBinder interface. This option gives you unlimited capabilities to map the request data to the model.
Related
When creating Employee entity you are supposed to select MeetingCenterfrom DropDownList. All MeetingCenters show just fine in DropDownList with their Names, but when some of them is selected and Employee is created Meeting Center is null. Im using NoSQL DocumentDB database.
Controller:
[ActionName("Create")]
public async Task<IActionResult> CreateAsync()
{
ViewBag.MeetingCentersList = await _meetingCenterReposiotry.GetItemsAsync();
return View();
}
Create View:
#model Employee
#using (Html.BeginForm("Create", "Home", FormMethod.Post, new { #class = "form-horizontal" }))
{
...
<div class="form-group">
#Html.LabelFor(MeetingCenter => Model.MeetingCenter, new { #class = "control-label" })
#Html.DropDownListFor(MeetingCenter => Model.MeetingCenter, new SelectList(ViewBag.MeetingCentersList, "MeetingCenterId", "Name"), new { #class = "form-control" })
</div>
...
}
Piece of Employee Model
public class Employee
{
...
[JsonProperty(PropertyName = "id")]
public string EmployeeId { get; set; }
[JsonProperty(PropertyName = "meetingCenter")]
public MeetingCenter MeetingCenter { get; set; }
...
}
Piece of MeetingCenter Model
public class MeetingCenter
{
...
[JsonProperty(PropertyName = "id")]
public string MeetingCenterId { get; set; }
...
}
With your current code, the DropDownListFor helper will render a SELECT element with options, which has the MeetingCenterId as the value attribute and the Name as the Text. The SELECT element's name attribute value will be MeetingCenter. So when the form is submitted the form data will look like this
MeetingCenter: 2
Assuming user selected the option with value "2".
But the MeetingCenter property of your view model(Employee) is not a numeric type, it is a complex type(MeetingCenter). So model binder cannot map this value to MeetingCenter property of your view model.
You can render the SELECT element with the name MeetingCenter.MeetingCenterId and then model binder will be able to map the posted form data as the input element name matches with the naming-structure of your view model.
So you should render something like this inside your form.
<select name="MeetingCenter.MeetingCenterId">
</select>
You can generate the above markup by using the SELECT tag helper and specifying MeetingCenter.MeetingCenterId as the asp-for property.
<select asp-for="MeetingCenter.MeetingCenterId"
asp-items="#(new SelectList(ViewBag.MeetingCentersList,
"MeetingCenterId", "Title"))">
</select>
Now when the form is submitted, it will populate the MeetingCenter property of your view model and it's MeetingCenterId property.
If you want the full MeetingCenter property to be populated (properties other than MeetingCenterId, get the full object by querying the data provided by _meetingCenterReposiotry.GetItemsAsync() using the MeetingCenterId available to you in the HttpPost action. Something like this
var id = viewModel.MeetingCenter.MeetingCenterId;
var items = await _meetingCenterReposiotry.GetItemsAsync();
var item = items.FirstOrDefault(a=>a.MeetingCenterId==id);
// User item now
// May be do somethig like : viewModel.MeetingCenter = item;
I also suggest you to use the correct types. If MeetingCenterId is numeric value, use int as type instead of string
Please see the models below:
public class Apple //: Fruit
{
public string Description { get; set; }
public int Id { get; protected set; }
}
public class AppleModel
{
public int Id { get; set; }
public string Description { get; set; }
}
and the controller below:
[HttpPost]
public ActionResult Index(Apple apple)
{
return View();
}
[HttpGet]
public ActionResult Index()
{
var AppleModel = new AppleModel();
AppleModel.Id = 1;
AppleModel.Description = "Apple";
var Apple = AutoMapper.Mapper.Map<Apple>(AppleModel);
return View("View1",Apple);
}
and the view below:
#model PreQualification.Web.Controllers.Apple
#{
ViewBag.Title = "View1";
}
<h2>View1</h2>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>AppleModel</h4>
<hr />
#Html.ValidationSummary(true)
<div class="form-group">
#Html.LabelFor(model => model.Id)
<div class="col-md-10">
#Html.EditorFor(model => model.Id)
#Html.ValidationMessageFor(model => model.Id)
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.Description, new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Description)
#Html.ValidationMessageFor(model => model.Description)
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}
Apple.Id is 0 in the HttpPost method because it is a protected variable in the model. Is there anyway around this?
The reason I ask is because I am trying to use this as a model: https://github.com/nhibernate/NHibernate.AspNet.Identity/tree/master/source/NHibernate.AspNet.Identity and the id contained in the superclass is protected.
There are a couple ways around this, including a child class that implements a public whose setter applies the value to the protected id field.
However these are bandaids. Generally these problems are encountered because of a difference in how people view models. Either it is a reusable Data Transfer Object or it is not.
In the world where it is not, you have to shoehorn business objects into bindable models and always run into these weird problems.
In the world where they are, they are custom tailored to fit the data needs and are mapped into business objects with something like an automapper. More importantly, by making a model to fit this request, you protect against attacks on accidentally exposed parameters.
If the makeshift business object has public properties that change your behavior, they can be exploited by sending additional parameters back with the post request.
I know this does not specifically answer your question, but following the path where models are not DTO's is probably not the right answer either.
As elaborated by peewee_RotA, it's better to separate out the conceptual differences between a model used for a view and a domain model that will actually perform an action or is used to directly perform an action. To that end, you need a view model such as Apple
public class Apple
{
public string Description { get; set; }
public int Id { get; set; }
}
In your post action, you'd have;
[HttpPost]
public ActionResult Index(Apple apple)
{
// translate your view model to your domain model
AppleModel model = new AppleModel(apple.Id, apple.Description);
model.DoStuff();
}
In terms of NHiberhate though, it's not easy to instantiate the required object with the view model's ID and this is the crux of your issue I think. You're trying to use your domain model as a view model and the domain model is locked down...legitimately. It's job is to control how IdentityUser and the like are instantiated as they're meant to be generated by the associated factory classes, not MVC's model binder.
To that end, leave your view model as simple as possible and leverage NHibernate's factory classes to create the necessary Identity objects by looking up the ID. This link may shed some light on how to look up the user ID passed in by Apple.Id.
EDIT
I've done a little more digging on looking up the entity in NHibernate and the following post seems to do the basics. Does this get what you need?
I am trying to add a new record to a database that I've created. The database is called QUESTIONNAIRES and it has the columns: QuestionnaireUID, UserUID, QuestionnaireName, DateCreated, Link, and Image.
I want the user to specify the QuestionnaireName and provide an Image and I want to generate myself the QuestionnaireUID, UserUID, DateCreated, and Link. So far, this is my View() that represents this creation process:
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
// Hide QuestionnaireUID, UserUID, and Link from user. These fields will be generated instead of assigned by user input.
#Html.HiddenFor(model => model.QuestionnaireUID)
#Html.HiddenFor(model => model.UserUID)
#Html.HiddenFor(model => model.Link)
<div class="form-group"> <!-- Questionnaire name. -->
<h2>Name</h2>
<p> Please provide a name for your decision tree.<p>
<div class="col-md-10">
#Html.EditorFor(model => model.QuestionnaireName, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.QuestionnaireName, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group"> <!-- Questionnaire image. -->
<h2>Image</h2>
<p> Please provide a background image for your decision tree.</p>
<!-- ADD FILE IMAGES & ENCODE IN BINARY. -->
<div class="col-md-10">
#Html.EditorFor(model => model.Image, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.Image, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group btn_next"> <!-- Save and continue button. -->
<input type="submit" value="Save and Continue" class="btn">
</div>
}
The Questionnaire Controller methods that are being used are displayed below as well:
// GET: Questionnaires/Create
public ActionResult Create()
{
ViewBag.UserUID = new SelectList(db.Users, "UserUID", "FirstName");
return View();
}
// POST: Questionnaires/Create
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "QuestionnaireUID, UserUID, QuestionnaireName, DateCreated, Link, Image")] QUESTIONNAIRE questionnaire)
{
if (ModelState.IsValid)
{
db.QUESTIONNAIRES.Add(questionnaire);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(questionnaire);
}
As you can see, I've hidden away the three attributes that I want to generate in my View(). I now don't know where I generate and assign these values. Do I do this in my View() or in the Controller method? And what does this assignment look like?
I'd generate those values in the Controller on HttpGet, and I'd use a ViewModel.
Echoing mituw16's comment, using a ViewModel is a good way of keeping everything logically consistent and separated. There's some pretty good in-depth discussions and explanations of ViewModels elsewhere that are worth reading.
Your ViewModel could, for instance, look something (roughly) like this:
public class QuestionnaireViewModel
{
public Guid QuestionnaireUID { get; set; }
public Guid UserUID { get; set; }
public string QuestionnaireName { get; set; }
public DateTime DateCreated { get; set; }
public string Link { get; set; }
public Image Image { get; set; }
}
And it could be passed to the View like this:
[HttpGet]
public ActionResult Create()
{
var vm = new QuestionnaireViewModel();
vm.QuestionnaireUID = Guid.NewGuid();
vm.UserUID = Guid.NewGuid();
return View(vm);
}
When the form is posted, MVC can automatically interpret the incoming data as a QuestionnaireViewModel:
[HttpPost]
public ActionResult Create(QuestionnaireViewModel vm)
{
if (ModelState.IsValid)
{
// map the viewmodel properties onto the domain model object here
db.SaveChanges();
return RedirectToAction("Index");
}
return View(questionnaire);
}
A couple more points:
In this example, you'll see that it may not even be necessary to include the UID stuff in the ViewModel, as the ViewModel only cares about data gathered from/presented to the user. Further, unless the #Html.HiddenFors have some sort of functional purpose on the view, you might be able to leave them out and generate them on HttpPost.
If you're looking to "create new record with a combination of values being assigned from user input and being generated in ASP.NET MVC 4" (a.k.a. creating a form in MVC), then the more complex your model/viewmodel gets, the more I'd stay away from using ViewBag for these purposes.
This is the model I'm using for a view
public class MainRegisterViewModel
{
public RegisterViewModel RegisterModel { get; set; }
public RegisterFormValuesViewModel RegisterValues { get; set; }
}
RegisterFormValuesViewModel contains all the values for the controls (list of countries, states and stuff like that) and RegisterViewModel contains the information for a user.
Then I load the controls like this.
#model ProjetX.Models.MainRegisterViewModel
#{
IEnumerable<SelectListItem> countries = Model.RegisterValues.Countries.Select(x => new SelectListItem()
{
Text = x.Country,
Value = x.Id.ToString()
});
IEnumerable<SelectListItem> states = Model.RegisterValues.States.Select(x => new SelectListItem()
{
Text = x.State,
Value = x.Id.ToString()
});
}
<div class="container">
<div class="row">
<div class="col-md-12">
#using (Html.BeginForm("Register", "Account", FormMethod.Post, new { #class = "form-horizontal", role = "form" }))
{
....
<div class="form-group">
#Html.LabelFor(m => m.RegisterModel.Country, new { #class = "col-md-2 control-label" })
<div class="col-md-10">
#Html.DropDownListFor(m => m.RegisterModel.Country, countries, new { #class = "form-control" })
</div>
</div>
....
Also the Register function takes a MainRegisterViewModel as parameter.
public async Task<ActionResult> Register(MainRegisterViewModel model)
The problem is when I submit the form, RegisterFormValuesViewModel is NULL.
Any ideas why?
Thank you
Context
I'm doing this because I load the RegisterFormValuesViewModel from an API and I'm trying to call it only once. The problem was when a user POST a form with errors and you return the view back, I had to call the API again to get RegisterFormValuesViewModel.
Before this it was only one model and a viewbag for RegisterFormValuesViewModel but I had to call the API every time the form was loaded because the viewbag wasn't posted. That's why I thought I could use 2 models and POST them both.
If you want the values of RegisterFormValuesViewModel to be posted, they need to included in the form or other location that the ModelBinder looks for values. The (default) model binder will pick up values from the action params, Request.Form, route data, and Request.QueryString (I think Request.Files is included too).
If your RegisterFormValuesViewModel is expensive to create, you can add it's values as hidden fields that are posted with the form or implement a custom ValueProviderFactory that works with application or session state.
Im using ASP.Net MVC 5.
I have two simple classes; Student and Course, like this;
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Course
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Student> Students { get; set; }
}
I want to create a new Course with optional many Students.
The student(s) form/view will be rendered as a partail view (insde the Course-view).
Right now I have a Create-View that is strongly type to Course.
This view only have 1 textbox - name of the Course.
I render the partial view that is strongly typed to Student.
To simplify i just want to add 1 student to the List.
I would like pass the student data to the course object and then "move on" to the controller.
Can anyone help me with this approach of passing data from a partitial view, or give me a hint of how its done in MVC? ;)
Ok found out what I was doing wrong. First off I donwloaded the Html helper, BeginCollectionItem. Real nice if you want to dynamically add and remove fields/textboxes that will be added to your model.
First off send an empty object to to your view to work with (from your controller).
I just passed in a new Course object. Ctor of Course creates a new List with 1 student object.
Then i used RenderPartial to display a partailview + the student item.
#foreach(var student in Model.Students)
{
RenderPartial("_Student", student);
}
This view looks like this:
#model Project.Data.Entities.Student
<div class="AddStudent form-group">
#using (Html.BeginCollectionItem("students"))
{
#Html.Label("Name:", new { #class = "control-label col-md-2" })
<div class="col-md-8">
#Html.TextBoxFor(x => x.Name)
<button type="button" class="deletButton btn btn-default">Remove</button>
#Html.ValidationMessageFor(model => model.Name)
</div>
}
</div>
I render a button that is hooked up to delete (jquery) the student field.
When i want to add more students to my Course i just use an ajax call to add more partial "_Student" views.
<div>
#Ajax.ActionLink("Add more...", "NewStudentRow", "Course", new AjaxOptions()
{
InsertionMode = InsertionMode.InsertAfter,
UpdateTargetId = "students"
}, new { #class = "btn btn-default" })
</div>
The NewStudentRow method in my controller looks like this:
public PartialViewResult NewStudentRow ()
{
return PartialView("_Student", new Student());
}
Pretty simple if you just use the http://www.nuget.org/packages/BeginCollectionItem/
You can solve this by having more than one partial view..
Pseudo-code:
CourseView:
<TextBox>#Model.Name</TextBox>
#foreach(var student in Model.Students)
{
RenderPartial("ShowStudent");
}
RenderPartial("AddStudent");
The AddStudentView conains all fields you need to provide to save a student to database. In the action you take the input parameters, save the new student and redirect ( something like return RedirectToAction("Course", new { id = student.CourseId }) ) to the Course view. The course view will then be loaded including the new student.
You could also do all of this with ajax to prevent postback, but as you haven't specified any desire to prevent postback I think this would be a good solution.