I'm wondering what's the general approach of passing a list of lookup values to a view in MVC. Currently I have 2 db tables and I'm using db first EF6 to interface. My main table has a lookup table and I want to populate a dropdownlist of my view with all the values of the lookup so that the user can pick when creating and editing.
Employee Table
id primary key
name varchar
department id - this is the id of the department in the lookup
Department table
id primary key
name varchar
Is it best to create a partial class for the employee model and add a new property called allDepartments and then in my controller call a method that gets all the departments before passing the model to the view, or is it better to dump the departments in the viewbag/viewdata dictionary?
What is the general approach here?
You need to create a ViewModel like this:
public class EmployeeViewModel
{
public string Name { get; set; }
[Required(ErrorMessage = "...")] // to protect against under-posting attacks
[Display(Name = "Department")]
public int? DepartmentId { get; set; }
public IEnumerable<SelectListItem> Departments { get; set; }
}
Controller:
public ActionResult Create()
{
var employeeViewModel = new EmployeeViewModel();
employeeViewModel.Departments = GetDepartments().Select(option => new SelectListItem
{
Text = option.name,
Value = option.Id.ToString()
});
return View(employeeViewModel);
}
// Post
public ActionResult Create(EmployeeViewModel model)
{
// Map ViewModel to Entity and Save to db...
}
View:
#model EmployeViewModel
<div class="form-group">
#Html.LabelFor(model => model.Name)
#Html.TextBoxFor(model => model.Name, new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.Name)
</div>
<div class="form-group">
#Html.LabelFor(model => model.DepartmentId)
#Html.DropDownListFor(model => model.DepartmentId, Model.Departments, "Choose...")
#Html.ValidationMessageFor(model => model.DepartmentId)
</div>
Related
I have an ever-changing list of Industries that I'd like a user to select from when creating a new Survey.
I could accomplish this with either a ViewModel or ViewBag (I think). I'm attempting to do it with ViewBag. Getting the DropDownListFor error:
CS1928 HtmlHelper<Survey> does not contain a definition for DropDownListFor and the best extension method overload SelectExtensions.DropDownListFor<TModel, TProperty>(HtmlHelper<TModel>, Expression<Func<TModel, TProperty>>, IEnumerable<SelectListItem>, object) has some invalid arguments 2_Views_Surveys_Create.cshtml
Survey model, with foreign key to Industry:
public class Survey
{
[Key]
public int id { get; set; }
[Display(Name = "Survey Name")]
public string name { get; set; }
[Display(Name = "Industry")]
public int industryId { get; set; }
[ForeignKey("industryId")]
public virtual Industry industry { get; set; }
}
Controller to load Industries SelectList into ViewBag:
// GET: Surveys/Create
public ActionResult Create()
{
ViewBag.Industries = new SelectList(db.industry, "Id", "Name");
return View();
}
Create view:
<div class="form-group">
#Html.LabelFor(model => model.industryId, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownListFor(model => model.industryId, ViewBag.Industries, "-Select Industry-")
#Html.ValidationMessageFor(model => model.industryId, "", new { #class = "text-danger" })
</div>
</div>
Properties of the ViewBag have no type that the compiler can use to decide which overload of the method to call. Help the compiler by using an explicit cast.
#Html.DropDownListFor(model => model.industryId, (IEnumerable<SelectListItem>)ViewBag.Industries, "-Select Industry-")
I have created a View Model called CompetitionRoundModel which is partially produced below:
public class CompetitionRoundModel
{
public IEnumerable<SelectListItem> CategoryValues
{
get
{
return Enumerable
.Range(0, Categories.Count())
.Select(x => new SelectListItem
{
Value = Categories.ElementAt(x).Id.ToString(),
Text = Categories.ElementAt(x).Name
});
}
}
[Display(Name = "Category")]
public int CategoryId { get; set; }
public IEnumerable<Category> Categories { get; set; }
// Other parameters
}
I have structured the model this way because I need to populate a dropdown based on the value stored in CategoryValues. So for my view I have:
#using (Html.BeginForm())
{
<div class="form-group">
#Html.LabelFor(model => model.CategoryId, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownListFor(model => model.CategoryId, Model.CategoryValues, new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.CategoryId, "", new { #class = "text-danger" })
</div>
</div>
// Other code goes here
}
I have selected model.CategoryId in the DropDownListFor() method since I want to bind the selected value to CategoryId. I really don't care for CategoryValues, I just need it to populate the DropDown.
My problem now is that when my Controller receives the values for my Model in the action method, CategoryValues is null which causes the system to throw a ArgumentNullException (the line that is highlighted is the return Enumerable line.
I have even tried [Bind(Exclude="CategoryValues")] but no change at all. Any help would be much appreciated.
Your not (and should not be) creating form controls for each property of each Category in your IEnumerable<Category> collection so in your POST method, the value of Categories is null (it never gets initialized). As soon as you attempt CategoryValues and exception is thrown by your .Range(0, Categories.Count()) line of code in the getter.
Change you view model to give CategoryValues a simple geter/setter, and delete the Categories property
public class CompetitionRoundModel
{
public IEnumerable<SelectListItem> CategoryValues { get; set; }
[Display(Name = "Category")]
public int CategoryId { get; set; }
.... // Other properties
}
and populate the SelectList in the controller methods, for example
var categories db.Categories; // your database call
CompetitionRoundModel model = new CompetitionRoundModel()
{
CategoryValues = categories.Select(x => new SelectListItem()
{
Value = x.Id.ToString(),
Text = x.Name
},
....
};
return View(model);
or alternatively
CompetitionRoundModel model = new CompetitionRoundModel()
{
CategoryValues = new SelectList(categories, "Id", "Name" ),
Note also that if you return the view (because ModelState is invalid, the you need to repopulate the value of CategoryValues (refer The ViewData item that has the key 'XXX' is of type 'System.Int32' but must be of type 'IEnumerable' for more detail)
Since CategoryValues just populates the drop down, it will never post back to the server and you'll need to rebuild the list from the database before using it in the GET or POST operation. The CategoryId property is the value that will be posted back to the server from the DropDownList.
I have a question about setting up a viewmodel when you use the strongly typed helpers (HTML.EditorFor, etc.) and a viewmodel in ASP.NET MVC. I am working with MVC5, but I would imagine my question is also applicable to other versions.
For this example, I'm working with the create of the CRUD process.
In the example, the user enters the name and address of a person and city is selected from a drop down.
Here is the model:
public class Person
{
[Key]
public int PersonID { get; set; }
[ForeignKey("City")]
public int CityID { get; set; }
public string Name {get; set;}
public string address {get; set;}
//Navigational property
public virtual City City { get; set; }
}
Here is the viewmodel:
public class PersonCreateViewModel
{
public SelectList cities {get; set;}
public Person person { get; set; }
}
Here is the Action Method from the controller used to pass back the view for the create page:
public ActionResult Create()
{
CreateViewModel viewmodel = new CreateViewModel();
viewmodel.cities = new SelectList(db.Cities, "CityID", "name");
return View(viewmodel);
}
Here is part of my view:
<div class="form-group">
#Html.LabelFor(model => model.person.name, new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.person.name)
#Html.ValidationMessageFor(model => model.person.name)
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.person.address, new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.person.address)
#Html.ValidationMessageFor(model => model.person.address)
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.person.CityID, "CityID", new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownList("cities")
#Html.ValidationMessageFor(model => model.person.CityID)
</div>
</div>
I declare the model for my view as such:
#model TestProjects.ViewModels.PersonCreateViewModel
And Lastly, the http post method in the controller:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include="PersonID,CityID,nameaddress")] Person person)
{
if (ModelState.IsValid)
{
//Add to database here and return
}
//return back to view if invalid db save
return View(person);
}
So at one point I had all of this working. Then I decided I wanted to use the ViewModel approach. I still don't have it working, but here are some questions:
In the view, I reference the properties of the model with model.person.address. Is this the proper way to do this? I noticed that when it generates the html, it names the field person_address, etc.
So should I just change the Bind properties in the http post controller mehtod to reflect this? But if I change this, the properties will no longer match up with the person object causing a disconnect.
Or should I change my view model? And instead of having a person type in my ViewModel, copy/paste all of the fields from the model into the ViewModel? I guess this would also work, but is that the typical way it is done? It seems redundant to list out every property of the model when I could just have an instance if the model in the viewmodel?
In the view, I reference the properties of the model with model.person.address. Is this the proper way to do this? I noticed that when it generates the html, it names the field person_address, etc.
Yes, that is the correct way to reference model properties. More accurately, since model in your helper expressions is a reference to the Func's input parameter, it could be anything. The following would work just as well:
#Html.EditorFor(banana => banana.person.address)
So should I just change the Bind properties in the http post controller mehtod to reflect this? But if I change this, the properties will no longer match up with the person object causing a disconnect.
You don't need the bind parameters at all. What you should do is take all reference to your data entities (i.e. Person) out of your view model completely (otherwise using the view model is a little pointless as it's tightly coupled with your data entities anyway) and give the view model properties that the view needs, e.g.:
public class PersonCreateViewModel
{
public SelectList Cities { get; set; }
public string Address { get; set; }
public string Name { get; set; }
...
}
They should then bind back by default to the same model, presuming your view is correct:
public ActionResult Create (PersonCreateViewModel model)
{
// Map PersonCreateViewModel properties to Person properties
}
I am new to Asp.net MVC and could really use some clarification on how View models work.
From my understanding, View models are used to only expose necessary fields from the domain model to the Views. What I am finding hard to understand is that domain models are linked to the Db via Dbset. So it makes sense to me that when data is posted to a controller using a domain model, that this data can find its way into the Db.
From the examples of View models I have seen, they are not referenced by a Dbset. So how does data posted to a View model find its way into the database. Does EF just match the fields from the View model to fields which match from the domain model?
thanks for your help
As Jonathan stated, AutoMapper will help you map your ViewModel entities to your Domain model. Here is an example:
In your view you work with the View Model (CreateGroupVM):
#model X.X.Areas.Group.Models.CreateGroupVM
#using (Html.BeginForm(null,null, FormMethod.Post, new { #class="form-horizontal", role="form"}))
{
#Html.ValidationSummary()
#Html.AntiForgeryToken()
#Html.LabelFor(model => model.Title, new { #class = "col-lg-4 control-label" })
#Html.TextBoxFor(model => model.Title, new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.Title)
#Html.LabelFor(model => model.Description, new { #class = "col-lg-4 control-label" })
#Html.TextBoxFor(model => model.Description, new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.Description)
#Html.LabelFor(model => model.CategoryId, new { #class = "col-lg-4 control-label" })
#Html.DropDownListFor(x => x.CategoryId, Model.Categories)
#Html.ValidationMessageFor(model => model.CategoryId)
<div class="form-group">
<div class="col-lg-offset-4 col-lg-8">
<button type="submit" class="btn-u btn-u-blue">Create</button>
</div>
</div>
}
ViewModel (CreateGroupVM.cs):
Notice how we pass in a list of Categories - you could not do this had you strictly used your domain model because you cant pass a list of categories in the Group model. This gives us strongly typed helpers in our views, and no ViewBag usage.
public class CreateGroupVM
{
[Required]
public string Title { get; set; }
public string Description { get; set; }
[DisplayName("Category")]
public int CategoryId { get; set; }
public IEnumerable<SelectListItem> Categories { get; set; }
}
Domain Model (Group.cs):
public class Group
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public int CategoryId { get; set; }
public int CreatorUserId { get; set; }
public bool Deleted { get; set; }
}
In your HttpPost Create Action - you let AutoMapper do the mapping then save to the DB. Note that by default AutoMapper will map fields that are the same name. You can read https://github.com/AutoMapper/AutoMapper/wiki/Getting-started to get started with AutoMapper.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(CreateGroupVM vm)
{
if (ModelState.IsValid)
{
var group = new InterestGroup();
Mapper.Map(vm, group); // Let AutoMapper do the work
db.Groups.Add(group);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(vm);
}
The view models are in no way tied to your database. You would need to create a new domain model and populate it with the data from the view model in order to save it to the database. Of course, having to do that is very annoying and someone created AutoMapper to handle that.
With automapper you could just match the properties from your view models to properties in the domain model and then add them to the database as needed.
I'm trying my hardest to use ViewModels correctly in my web application, but I'm running into various problems. One of which, is if I set a breakpoint just after I post using a Create action, my viewModel hasn't stored any of my form values. I must be doing something wrong, but I've tried a few things. Including the code below, where I name the form items the same as the viewModel fields to see if that helps.
I'm also wondering what exactly properties in your viewmodel should represent. I've seen people use different things in blog posts and whatnot.
If the view is going to render a select list, I'm under the impression the viewmodel should hold an IEnumerable SelectListItem for this as below. Yet I've seen people use IEnumerable Entity instead, to represent the type the select list represents.
Can anybody shed some light on this for me? I scrapped my entire business logic last night so I could start a fresh and try and do it correctly.
My ViewModel:
public class ServerCreateViewModel
{
public int Id { get; set; }
// CompanyName represents a field in the Company model. I did this to see if
// it would help with model binding. Beforehand it was Companies to represent the type. I've done the same for the rest of them, so I wont comment on this again.
public IEnumerable<SelectListItem> CompanyName { get; set; }
// Represents the Game model.
public IEnumerable<SelectListItem> GameTitle { get; set; }
//Represents the Location model, etc...
public IEnumerable<SelectListItem> City { get; set; }
public IEnumerable<SelectListItem> NumberOfPlayers { get; set; }
public IEnumerable<SelectListItem> CurrencyAbbreviation { get; set; }
}
My Controller action:
public ActionResult Create()
{
var viewModel = new ServerCreateViewModel();
viewModel.CompanyName = new SelectList(_dataService.Companies.All(), "Id", "CompanyName");
viewModel.GameTitle = new SelectList(_dataService.Games.All(), "Id", "GameTitle");
viewModel.City = new SelectList(_dataService.Locations.All(), "Id", "City");
viewModel.NumberOfPlayers = new SelectList(_dataService.ServerPlayers.All(), "Id", "NumberOfPlayers");
return View(viewModel);
}
[HttpPost]
public ActionResult Create(FormCollection collection, ServerCreateViewModel viewModel)
{
try
{ // I put a breakpoint in here to check the viewModel values.
// If I dont pass the viewModel into the constructor, it doesnt exist.
// When I do pass it in, its empty.
return Content("Success");
}
catch
{
return Content("Fail");
}
}
My View:
#model GameserverCompare.ViewModels.Server.ServerCreateViewModel
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
<fieldset>
<legend>Server</legend>
#Html.HiddenFor(m => m.Id)
<div class="editor-label">
#Html.LabelFor(model => model.CompanyName)
</div>
<div class="editor-field">
#Html.DropDownListFor(m => Model.CompanyName, Model.CompanyName)
#Html.ValidationMessageFor(model => model.CompanyName)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.GameTitle)
</div>
<div class="editor-field">
#Html.DropDownListFor(m => Model.GameTitle, Model.GameTitle)
#Html.ValidationMessageFor(model => model.GameTitle)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.City)
</div>
<div class="editor-field">
#Html.DropDownListFor(m => Model.City, Model.City)
#Html.ValidationMessageFor(model => model.City)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.NumberOfPlayers)
</div>
<div class="editor-field">
#Html.DropDownListFor(m => Model.NumberOfPlayers, Model.NumberOfPlayers)
#Html.ValidationMessageFor(model => model.NumberOfPlayers)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
Since you're using SelectList properties in the form model, you will need to have a different model to represent the selected values in those lists:
public class ServerCreatePostbackModel
{
public int Id { get; set; }
// CompanyName represents a field in the Company model.
public string CompanyName { get; set; }
// Represents the Game model.
public string GameTitle { get; set; }
//Represents the Location model, etc...
public string City { get; set; }
public int NumberOfPlayers { get; set; }
public string CurrencyAbbreviation { get; set; }
}
Have your HttpPost action take one of these as its argument.
Oh, and be sure to use HiddenFor for the Id property, so it gets sent back with the other data.