I'm working with ASP.NET MVC 4 and Entity Framework. In my database, I have a table Subscription which represents a subscription to public transports. This subscription can provide access to several public transport companies (so a subscription could have 1, 2, 3, ... companies) then it is a Many-to-Many relation between these tables (I have an intermediate table between them).
I want to allow the creation of a subscription throught a page which will contain a field Amount of the subscription and the available companies by checkboxes. Every checkbox represents an existing company (a company stored in my database).
Any idea about how to do that? I've read this ASP.NET MVC Multiple Checkboxes but it was not really helpful.
EDIT : Here is my tables diagram.
You start with two view models. The first one which represents a selected company...
public class CompanySelectViewModel
{
public int CompanyId { get; set; }
public string Name { get; set; }
public bool IsSelected { get; set; }
}
...and the second one for the subscription to create:
public class SubscriptionCreateViewModel
{
public int Amount { get; set; }
public IEnumerable<CompanySelectViewModel> Companies { get; set; }
}
Then in the SubscriptionControllers GET action you load the companies from the database to initialize the view model:
public ActionResult Create()
{
var viewModel = new SubscriptionCreateViewModel
{
Companies = _context.Companies
.Select(c => new CompanySelectViewModel
{
CompanyId = c.CompanyId,
Name = c.Name,
IsSelected = false
})
.ToList()
};
return View(viewModel);
}
Now, you have a strongly typed view for this action:
#model SubscriptionCreateViewModel
#using (Html.BeginForm()) {
#Html.EditorFor(model => model.Amount)
#Html.EditorFor(model => model.Companies)
<input type="submit" value="Create" />
#Html.ActionLink("Cancel", "Index")
}
To get the company checkboxes rendered correctly you introduce an editor template. It must have the name CompanySelectViewModel.cshtml and goes into the folder Views/Subscription/EditorTemplates (create such a folder manually if it doesn't exist). It's a strongly typed partial view:
#model CompanySelectViewModel
#Html.HiddenFor(model => model.CompanyId)
#Html.HiddenFor(model => model.Name)
#Html.LabelFor(model => model.IsSelected, Model.Name)
#Html.EditorFor(model => model.IsSelected)
Name is added as hidden field to preserve the name during a POST.
Obviously you have to style the views a bit more.
Now, your POST action would look like this:
[HttpPost]
public ActionResult Create(SubscriptionCreateViewModel viewModel)
{
if (ModelState.IsValid)
{
var subscription = new Subscription
{
Amount = viewModel.Amount,
Companies = new List<Company>()
};
foreach (var selectedCompany
in viewModel.Companies.Where(c => c.IsSelected))
{
var company = new Company { CompanyId = selectedCompany.CompanyId };
_context.Companies.Attach(company);
subscription.Companies.Add(company);
}
_context.Subscriptions.Add(subscription);
_context.SaveChanges();
return RedirectToAction("Index");
}
return View(viewModel);
}
Instead of using Attach you can also load the company first with var company = _context.Companies.Find(selectedCompany.CompanyId);. But with Attach you don't need a roundtrip to the database to load the companies to be added to the collection.
(Edit 2: In this answer is a continuation for the Edit actions and views with the same example model.)
Edit
Your model is not really a many-to-many relationship. You have two one-to-many relationships instead. The PublicTransportSubscriptionByCompany entity is not needed - normally. If you have a composite primary key in that table made of Id_PublicTransportSubscription, Id_PublicTransportCompany and remove the id column Id_PublicTransportSubscriptionByCompanyId EF would detect this table schema as a many-to-many relationship and create one collection in each of the entities for subscription and company and it would create no entity for the link table. My code above would apply then.
If you don't want to change the schema for some reason you must change the POST action like so:
[HttpPost]
public ActionResult Create(SubscriptionCreateViewModel viewModel)
{
if (ModelState.IsValid)
{
var subscription = new Subscription
{
Amount = viewModel.Amount,
SubscriptionByCompanies = new List<SubscriptionByCompany>()
};
foreach (var selectedCompany
in viewModel.Companies.Where(c => c.IsSelected))
{
var company = new Company { CompanyId = selectedCompany.CompanyId };
_context.Companies.Attach(company);
var subscriptionByCompany = new SubscriptionByCompany
{
Company = company
};
subscription.SubscriptionByCompanies.Add(subscriptionByCompany);
}
_context.Subscriptions.Add(subscription);
_context.SaveChanges();
return RedirectToAction("Index");
}
return View(viewModel);
}
I prefer this answer: Saving Many to Many relationship data on MVC Create view
If you are doing database first, then just skip to the viewmodel part of section 1.
Just an extension to Slauma's answer. In my case i had to represent many-to-many like a table between Products and Roles, first column representing Products, the header representing Roles and the table to be filled with checkboxes to select roles for product.
To achieve this i have used ViewModel like Slauma described, but added another model containing the last two, like so:
public class UserViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public IEnumerable<ProductViewModel> Products { get; set; }
}
public class ProductViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public IEnumerable<RoleViewModel> Roles { get; set; }
}
public class RoleViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public bool IsSelected { get; set; }
}
Next, in Controller we need to fill data:
UserViewModel user = new UserViewModel();
user.Name = "Me";
user.Products = new List<ProductViewModel>
{
new ProductViewModel
{
Id = 1,
Name = "Prod1",
Roles = new List<RoleViewModel>
{
new RoleViewModel
{
Id = 1,
Name = "Role1",
IsSelected = false
}
// add more roles
}
}
// add more products with the same roles as Prod1 has
};
Next, in View:
#model UserViewModel#using (Ajax.BeginForm("Create", "User",
new AjaxOptions
{
HttpMethod = "POST",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "divContainer"
}))
{
<table>
<thead>
<tr>
<th>
</th>
#foreach (RoleViewModel role in Model.Products.First().Roles.ToList())
{
<th>
#role.Name
</th>
}
</tr>
</thead>
<tbody>
#Html.EditorFor(model => model.Products)
</tbody>
</table>
<input type="submit" name="Create" value="Create"/>
}
As you see, EditorFor is using template for Products:
#model Insurance.Admin.Models.ProductViewModel
#Html.HiddenFor(model => model.Id)
<tr>
<th class="col-md-2 row-header">
#Model.Name
</th>
#Html.EditorFor(model => model.Roles)
</tr>
This template uses another template for Roles:
#model Insurance.Admin.Models.RoleViewModel
#Html.HiddenFor(model => model.Id)
<td>
#Html.EditorFor(model => model.IsSelected)
</td>
And voila, we have a table containing first column Products, the header contains Roles and the table is filled with checkboxes. We are posting UserViewModel and you will see that all the data are posted.
Related
I have a form that writes out an Approve/Deny radio button for each record. I'm trying to figure out how to use the HttpPost to loop through each and determine if the radio button is selected and if so, which one was selected.
Doing some research I see that some use the Form collection for the form and in one example I found where the user used the forms ViewModel (which is what I normally do). However, when I try either one I'm coming up empty handed.
Here is my form. I'm writing out each record in a List to a table. I've tried both the Html.RadioButton and Html.RadioButtonFor to create them. I also have a comments textbox underneath the table where someone can put in some comments. Here is a snippet of the View.
<tbody>
#foreach (var item in Model.listPendingExceptions)
{
<tr>
<td>#Html.RadioButton("rdo" + item.RID, "A")</td>
<td>#Html.RadioButton("rdo" + item.RID, "D")</td>
<td>#item.Shift_Date.ToShortDateString()</td>
</tr>
}
</tbody>
#Html.TextAreaFor(m => m.ExceptionComment, new { cols = 200, #rows = 4, #maxlength = "100", #class = "form-control", #placeholder = "100 character limitation", #autofocus = "autofocus" })
In my HttpPost I've tried using the form collection. However, what I've found is to look in the AllKeys list. When I view my Post the only thing in the AllKeys is the comment's TextBox value.
When I use a ViewModel in the HttpPost, the list of exceptions that I used to populate the table in the View is NULL. I would expect that since I didn't store the list in a hidden field.
How can I loop through each record, determine which if any radio button has been selected, as well as get the text from the Comments textbox?
UPDATE for EditTemplate
I created the folder structure for EditorTemplates in the Views.
I already had a ViewModel with a List of Exceptions but I did move the SelectedApproval from the main VM to the list of Exceptions.
public class ReportPendingExceptionsViewModel
{
public List<PendingException> listPendingExceptions { get; set; }
public bool IsAdmin { get; set; }
[Required(ErrorMessage = "*Required")]
public string ExceptionComment { get; set; }
}
public class PendingException
{
public int RID { get; set; }
public DateTime Shift_Date { get; set; }
public string Shift_Text { get; set; }
public string Emp_Id { get; set; }
public string Emp_Name { get; set; }
public string Last_Name { get; set; }
public string First_Name { get; set; }
public string Comment_Text { get; set; }
public string SelectedApproval { get; set; }
}
I then created a Razor View for the Table rows.
#model ProjectName.Models.ViewModels.PendingException
<tr>
<td>#Html.RadioButtonFor(e=>e.SelectedApproval,"A")</td>
<td>#Html.RadioButtonFor(e => e.SelectedApproval, "D")</td>
<td>#Model.Shift_Date.ToShortDateString()</td>
<td>#Model.Emp_Name</td>
<td>#Model.Shift_Text</td>
<td>#Model.Comment_Text</td>
<td></td>
</tr>
I then updated my main View to use the EditFor.
<thead>
<tr>
<th style="width:80px;">Approve</th>
<th style="width:80px;">Deny</th>
<th>Shift Date</th>
<th>Employee</th>
<th>Schedule</th>
<th>Comments</th>
<th></th>
</tr>
</thead>
<tbody>
#Html.EditorFor(f => f.listPendingExceptions)
</tbody>
However, when I run it, all I get is the RID values. So, I must be missing something. Here is the output from the View Source.
Did I miss a step?
public class ExceptionModel
{
public int Id { set; get; }
public bool IsApproved { set; get; }
public DateTime ShiftDate { set; get; }
}
public class MainModel
{
public string Comment { set;get;}
public List<ExceptionModel> lst_Exception { set;get;}
}
//this is get request action method
public ActionResult Create()
{
MainModel model = new MainModel();
model.lst_Exception = new List<ExceptionModel>()
{
new ExceptionModel() {Id = 1,IsApproved = false, ShiftDate = DateTime.Now},
new ExceptionModel() {Id = 2,IsApproved = false, ShiftDate = DateTime.Now},
new ExceptionModel() {Id = 3,IsApproved = false, ShiftDate = DateTime.Now}
};
return View(model);
}
//this is view for action method
#model MainModel
#using(Html.BeginForm())
{
<table>
<thead>
<tr>
<th>Approve</th>
<th>Deny</th>
<th>Shift Date</th>
</tr>
</thead>
<tbody>
#for (var item = 0; item < Model.lst_Exception.Count(); item++)
{
<tr>
<td>#Html.RadioButtonFor(model=>model.lst_Exception[item].IsApproved, "Approve")</td>
<td>#Html.RadioButtonFor(model=>model.lst_Exception[item].IsApproved, "Deny")</td>
<td><span>#Model.lst_Exception[item].ShiftDate</span>
#Html.HiddenFor(model => model.lst_Exception[item].ShiftDate})
</td>
</tr>
}
</tbody>
</table>
#Html.TextBoxFor(model=>model.Comment)
<input type="Submit" value="Submit" />
}
//this is Post action method
[HttpPost]
public ActionResult Create(MainModel model)
{
//here you can loop through model.lst_Exception to get the select values
//from the view
}
It is very easy to do this with Editor Templates.
Start with creating a view model for pending exception items
public class ExceptionVm
{
public int Id { set; get; }
public bool? IsApproved { set; get; }
public DateTime ShiftDate { set; get; }
}
and in your main view model, you will add a collection property which of of type
List<ExceptionVm>.
public class MyViewModel
{
public string Comment { set;get;}
public List<ExceptionVm> PendingExceptions { set;get;}
public MyViewModel()
{
PendingExceptions = new List<ExceptionVm>();
}
}
And in your GET action you initialize the view model object, load the PendingExceptions property
public ActionResult Create()
{
var vm = new MyViewModel();
vm.ExceptionVms = new List<ExceptionVm>()
{
new ExceptionVm() {Id = 1, ShiftDate = DateTime.Now.AddDays(-3)},
new ExceptionVm() {Id = 2, ShiftDate = DateTime.Now.AddDays(-2)},
new ExceptionVm() {Id = 3, ShiftDate = DateTime.Now.AddDays(-1)}
};
return View(vm);
}
Now, let's create an editor template. Create a new directory called EditorTemplates under ~/Views/YourControllerName/ or ~/Views/Shared/ and add a new razor view under that. Give the file the same name as our view model class, ExceptionVm.cshtml
Now add the below code to the editor template view. This basically render the 2 radio buttons and the date
#model ExceptionVm
<tr>
<td>#Html.RadioButtonFor(b=>b.IsApproved, true)</td>
<td>#Html.RadioButtonFor(b => b.IsApproved, false) </td>
<td> #Model.ShiftDate #Html.HiddenFor(x=>x.Id) </td>
</tr>
Now go to your main view, which is strongly typed to our MyViewModel class, and call the Html.EditorFor helper method and pass the PendingExceptions collection property to that
#model MyViewModel
#using(Html.BeginForm())
{
<table class="table">
<tbody>
#Html.EditorFor(f => f.PendingExceptions)
</tbody>
</table>
#Html.TextBoxFor(f => f.Comment)
<input type="Submit" value="Submit" class="btn btn-default" />
}
The call to the EditorFor will render a table row for each item in the PendingExceptions collection. When you submit the form, you can use the same MyViewModel class as the parameter and inspect the PendingExceptions property, iterate through each item and see whether it is true or false or null(if they have not selected anything)
[HttpPost]
public ActionResult Create(MyViewModel model)
{
// check model.PendingExceptions collection and each items IsApproved prop value
// to do : return something
}
If you do not want to allow null selection, change the IsApproved property type from bool? to bool
I have this Model:
public class ClassRoom
{
public List<Student> Students { get; set; }
}
public class Student
{
public int ID { get; set; }
public int Type { get; set; }
public string Name { get; set; }
public string LastName { get; set; }
}
The values of the ID and Type are already full, I need to create a view where the student can add his name and last name.
So I need to loop the list of students in the view.
I am doing the following:
#model Models.ClassRoom
#{
ViewBag.Title = "Classroom";
}
#if (Model != null)
{
<form action="Controller/Method">
foreach (var item in Model.Students)
{
#Html.HiddenFor(model => model.ID)
<div>
#Html.TextBoxFor(model => model.Name)
#Html.TextBoxFor(model => model.LastName)
</div>
}
<input type="submit">
</form>
}
I want to eventually submit a model of type Classroom with a list of students filled with Name and Last Name for each ID
But this is not working.
How can I bind values From the View to item on a certain index in a list?
For each ID in the hidden input,I want to save the written name and last name.
Please help
I need to create a form and submit the ClassRoom with a full List of Students eventually. What should be the types in my Controller method and views?
If you want to send your model back to controller, then you would need to generate naming correctly. There are several ways, one of them would look like this:
#using (Html.BeginForm("Index", "Home")) {
<div style="margin-top: 100px;">
#if (Model != null) {
for (var i = 0; i <= Model.Students.Count - 1; i++) {
#Html.HiddenFor(model => model.Students[i].ID)
<div>
#Html.TextBoxFor(model => model.Students[i].Name)
#Html.TextBoxFor(model => model.Students[i].LastName)
</div>
}
}
<input type="submit" value="Go"/>
</div>
}
And in controller dont forget to add:
[HttpGet]
public async Task<IActionResult> Index() {
var classroom = new ClassRoom();
... //add some students to the classroom
return View(classroom);
}
[HttpPost]
public async Task<IActionResult> Index(ClassRoom classRoom) {
...
}
Here can be found some more reading.
I have a model like this
public class Roles
{
[Key]
public string RoleId {get;set;}
public string RoleName {get;set;}
}
The challenge I have with this is creating a single view for the List and create. Each attempt I have made ended up in data type error. What do I do?
Answer 1:
In Order to hide some fields(as asked in Comments section) in #Html.EditorForModel() you have to use :
[ScaffoldColumn(false)] attribute
Ex :-
[Key]
[ScaffoldColumn(false)]
public string RoleId {get;set;}
Answer 2:
and creating and showing list on the same view :
Model :
public class Roles
{
[Key]
public string RoleId {get;set;}
public string RoleName {get;set;}
public List<Roles> Roleslist { get;set; } //Take a list in Model as shown
}
View :
<div id="mainwrapper">
#using (Html.BeginForm("// Action Name //", "// Controller Name //", FormMethod.Post, new { #id = "form1" }))
{
#Html.TextBoxFor(m => m.RoleName)
<input type="submit" value="Save"/>
}
<div id="RolesList">
#if(Model!=null)
{
if(Model.Roleslist!=null)
{
foreach(var item in Model.Roleslist) //Just Populate Roleslist with values on controller side
{
//here item will have your values of RoleId and RoleName
}
}
}
</div>
</div>
Also check your controller to ensure you are not passing Roles.ToList() to the CreateRole view.
You can do this:
[HttpGet]
public ActionResult CreateRole()
{
var roles = from ur in db.roles
orderby ur.RoleName
select ur;
ViewBag.Listroles = roles.ToList();
return View();
}
where db is your DbContext.
And your view should look like this:
#{
if(ViewBag.Listroles != null){
foreach (var roles in ViewBag.Listroles)
{
<tr>
<td>#roles.RoleName</td>
<td>
#Html.ActionLink("Edit", "EditRoles", new { id=roles.RoleId }) |
#Html.ActionLink("Details", "Details", new { id=roles.RoleId }) |
#Html.ActionLink("Delete", "Delete", new { id=roles.RoleId })
</td>
</tr>
}
}
}
Let me know if my answer helps.
I am new in MVC programming with razor and I need your advice on what is wrong with my code.
I have a Model
public class OrderDetails : OrderList
{
public string CompanyId { get; set; }
public List<OrderItems> OrdItems { get; set; }
}
public class OrderItems
{
public int Id { get; set; }
public string StopAddressName { get; set; }
}
I have a controller that populate data.
public ActionResult Edit()
{
OrderDetails ordDtl = new OrderDetails();
ordDtl.CompanyId = "1";
ordDtl.OrdItems = new List<OrderItems>();
for (int i = 1; i < 4; i++)
{
OrderItems tmp = new OrderItems();
tmp.Id = i;
tmp.StopAddressName = "Street " + i;
ordDtl.OrdItems.Add(tmp);
}
return View("EditOrder", ordDtl);
}
When I send it to view it shows all data. Here is my view.
#model OrderDetails
#using (Html.BeginForm("Edit", "Orderlayouts", FormMethod.Post))
{
<fieldset><legend>OrderDetail</legend>
<div class="editor-label">
#Html.LabelFor(model => model.CompanyId)
</div>
<div class="editor-field">
#Html.TextBoxFor(model => model.CompanyId)
</div>
<table>
<tr>
<td>id</td>
<td>Address</td>
</tr>
#foreach (var itm in Model.OrdItems)
{
<tr>
<td>#itm.Id</td>
<td>#itm.StopAddressName</td>
</tr>
}
</table>
<p>
<input type="submit" value="Edit" />
</p>
</fieldset>
}
When I click the submit button I have in controller CompanyID, but OrdItems values become null. Can you point out what is going wrong with my code.
Here is my controller.
[HttpPost]
public ActionResult Edit(OrderDetails Orderdt)
{
return View("EditOrder", Orderdt);
}
Any suggestions will be greatly appreciated.
I would try doing Something like this.
#for (int i = 0; i < Model.OrdItems.Count; i++)
{
#Html.DisplayFor(model => model.OrdItems[i].Id)#Html.HiddenFor(model => model.OrdItems[i].Id)
#Html.DisplayFor(model => model.OrdItems[i].StopAddressName)#Html.HiddenFor(model => model.OrdItems[i].StopAddressName)
}
Model binding is honestly the bane of my MVC work.
Here, however, you're going to see a form style post that only contains companyID - no other form fields for it to be using to bind are in your view.
I'm assuming OrderDetails is an entity (tied to a database table via Entity Framework). For all entities with navigation properties of a list-type, you must use an ICollection. Additionally, all navigation properties, whether a single foreign key or a list, must be virtual. EF does not actually return your class, but rather a proxy of your class with the navigation properties overridden to return the proper objects. If it's not virtual, EF can't make the override and you get a null value instead. So your model should look like:
public class OrderDetails : OrderList
{
public string CompanyId { get; set; }
public virtual ICollection<OrderItems> OrdItems { get; set; }
}
I have a problem while passing an object with HttpPost...
Once the form is submitted, the model is set "null" on the controller side, and I don't know where is the issue..
Here is my controller :
public ActionResult AddUser(int id = 0)
{
Group group = db.Groups.Find(id);
List<User> finalList = db.Users.ToList() ;
return View(new AddUserTemplate()
{
group = group,
users = finalList
});
//Everything is fine here, the object is greatly submitted to the view
}
[HttpPost]
public ActionResult AddUser(AddUserTemplate addusertemplate)
{
//Everytime we get in, "addusertemplate" is NULL
if (ModelState.IsValid)
{
//the model is null
}
return View(addusertemplate);
}
Here is AddUserTemplate.cs :
public class AddUserTemplate
{
public Group group { get; set; }
public User selectedUser { get; set; }
public ICollection<User> users { get; set; }
}
Here is the form which return a null value to the controller (note that the dropdown list is greatly populated with the good values) :
#using (Html.BeginForm()) {
<fieldset>
<legend>Add an user</legend>
#Html.HiddenFor(model => model.group)
#Html.HiddenFor(model => model.users)
<div class="editor-field">
//Here, we select an user from Model.users list
#Html.DropDownListFor(model => model.selectedUser, new SelectList(Model.users))
</div>
<p>
<input type="submit" value="Add" />
</p>
</fieldset>
}
Thanks a lot for your help
I tried your code and in my case the addusertemplate model was not null, but its properties were all null.
That's because of a few model binding issues: Html.HiddenFor and Html.DropDownListFor do not work with complex types (such as Group or User) (at least that's how it is by default).
Also, Html.HiddenFor cannot handle collections.
Here's how to solve these issues:
instead of #Html.HiddenFor(model => model.group) there should be one #Html.HiddenFor for each property of the group that you need bound
instead of #Html.HiddenFor(model => model.users) you need to iterate through the list of users and for each object add #Html.HiddenFor for each property of the user that you need bound
instead of #Html.DropDownListFor(model => model.selectedUser [...], create a property like int SelectedUserId {get;set;} and use that in the DropDownList (as it cannot handle complex types).
Here's the code that works:
1. The User and Group classes, as I imagined them to be:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Group
{
public int Id { get; set; }
public string Name { get; set; }
}
2. The adjusted AddUserTemplate class:
public class AddUserTemplate
{
public Group Group { get; set; }
public IList<User> Users { get; set; }
public int SelectedUserId { get; set; }
public User SelectedUser
{
get { return Users.Single(u => u.Id == SelectedUserId); }
}
}
The adjustments:
Users was changed from ICollection to IList, because we'll need to access elements by their indexes (see the view code)
added SelectedUserId property, that will be used in the DropDownList
the SelectedUser is not a readonly property, that returns the currently selected User.
3. The adjusted code for the view:
#using (Html.BeginForm())
{
<fieldset>
<legend>Add an user</legend>
#*Hidden elements for the group object*#
#Html.HiddenFor(model => model.Group.Id)
#Html.HiddenFor(model => model.Group.Name)
#*Hidden elements for each user object in the users IList*#
#for (var i = 0; i < Model.Users.Count; i++)
{
#Html.HiddenFor(m => m.Users[i].Id)
#Html.HiddenFor(m => m.Users[i].Name)
}
<div class="editor-field">
#*Here, we select an user from Model.users list*#
#Html.DropDownListFor(model => model.SelectedUserId, new SelectList(Model.Users, "Id", "Name"))
</div>
<p>
<input type="submit" value="Add" />
</p>
</fieldset>
}
Another option that does not require a bunch of hidden fields is to simply specify that you want the model passed to the controller. I think this is much cleaner.
#using(Html. BeginForm("action","controller", Model, FormMethod.Post)){
...
}