I have a scenario where I would like to have a list of editable items with per-item submit button, i.e. it should be allowed to submit only one item at a time.
I tried different approaches and the best answer I could find was this one: How to use multiple form elements in ASP.NET MVC, but the problem is that it assumes a single form and therefore a single Submit button.
So, here is what I tried so far. The Model:
namespace TestWebApplication4.Models
{
public class Entity
{
public int Id { get; set; }
[Required]
public string Name { get; set; }
}
public class TestModel
{
public IEnumerable<Entity> Entities { get; set; }
}
}
so I have a collection of Entity objects, which I want to display collectively, and edit and submit individually. The way I'm trying to do it is to have multiple forms on one View:
#using TestWebApplication4.Models
#model TestWebApplication4.Models.TestModel
#{
int i = 0;
foreach (Entity entity in Model.Entities)
{
i++;
<div>
#using (Html.BeginForm("Index"))
{
#Html.HiddenFor(model => entity.Id)
#Html.TextAreaFor(model => entity.Name, new {id = "entity_Name" + i})
#Html.ValidationMessageFor(model => entity.Name)
<button type="submit">Submit</button>
}
</div>
}
}
and the Controller:
namespace TestWebApplication4.Controllers
{
public class TestController : Controller
{
public ActionResult Index()
{
var model = BuildModel();
return View(model);
}
[HttpPost]
public ActionResult Index(Entity entity)
{
if (!ModelState.IsValid)
{
var model = BuildModel(entity);
return View(model);
}
return RedirectToAction("Index");
}
private static TestModel BuildModel(Entity entity = null)
{
var entities = new List<Entity>
{
new Entity {Id = 11, Name = "Product A"},
new Entity {Id = 12, Name = "Product B"},
new Entity {Id = 13, Name = "Product C"},
};
if (entity != null)
entities[entities.IndexOf(entities.Single(e => e.Id == entity.Id))] = entity;
return new TestModel {Entities = entities};
}
}
}
As you can see, I'm trying to receive individual Entity object in [HttpPost] Index action, and if it is invalid (Name value is not provided) to re-build the entire list and replace specific Entity with the invalid one (all of this happens in BuildModel method), to display the validation message. This however does not work properly, as the resulting view contains 3 entries, all with the same invalid Entity, i.e. I get three empty TextAreas and three "The Name field is required." validation messages.
Can anyone help me figure this out? I assume I'm doing something against the conventions and that it is probably completely wrong approach to this problem, however I could not find a solution anywhere myself, so any directions would be appreciated. Thanks in advance.
Related
Summary:
I'm trying to use two DropDownList controls to filter the data that is currently being sorted and displayed in a view.
What we are going to learn
Creating the ViewController for One to Many and Many-to-Many relationships that could Filter the data using DropDownList
Possible Causes
If my DropdownList code is not terrible wrong, The ViewModel I'm using to display the data has no proper support for the DropDownList items.
In other words, the RazorView and my ViewModels are not compatible for what I'm trying to achieve. If I try to change my ViewModel or RazorView, I get an eldless loop of errors for my existing code.
OR The Linq Query needs an expert attention
Here is FilterViewModel.cs
public IEnumerable <App> Apps { get; set; }
public IEnumerable <Language> Languages { get; set; }
public IEnumerable <Platform> Platforms { get; set; }
public IEnumerable <AgeGroup> AgeGroups { get; set; }
public IEnumerable <ProductCode> ProductCodes { get; set; }
Here is AppsController.cs
public ActionResult FilterApps(App app)
{
var apps = _context.Apps.ToList();
var languages = _context.Languages.ToList();
var productCodes = _context.ProductCodes.ToList();
var platforms = _context.Platforms.ToList();
var ageGroups = _context.AgeGroups.ToList();
var viewModel = new FilterViewModel
{
AgeGroups = ageGroups,
Languages = languages,
Platforms = platforms,
ProductCodes = productCodes,
Apps = apps
.Where(a => a.LanguageId == app.LanguageId && a.PlatformId == app.PlatformId)
// I also tried all possible combinations :(a.Lanage.Id etc)
};
return View("FilterApps", viewModel);
}
Here is the FilterApps.cshtml
#model Marketing.ViewModels.FilterViewModel
<h2>FilterApps</h2>
#using (Html.BeginForm("FilterApps", "Apps", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
<div class="form-group">
#Html.DropDownListFor( m => m.Languages,
new SelectList(Model.Languages, "Id", "Name"),"Select Language",
new { #class = "form-control", #id = "dropDown" })
#Html.DropDownListFor(m => m.Platforms,
new SelectList(Model.Platforms, "Id", "Name"), "Select Platform",
new { #onchange = "this.form.submit();",
#class = "form-control", #id = "dropDown" })
</div>
}
//The existing code below is working fine so far.
#foreach (var group in Model.AgeGroups)
{
<h4>#group.Name</h4>
#foreach (var app in Model.Apps.OrderBy(a => a.AppOrder))
{
if (app.AgeGroupId == group.Id)
{
#app.ProductCode.Name
#app.Name
#app.Platform.Name
}
}
}
Probably unnecessary but I hope the additional information will help.
Additional Information
The App.cs is referencing all other tables e.g.
public Language Language { get; set; }
public int LanguageId { get; set; }
public Platform Platform { get; set; }
public int PlatformId { get; set; }
and so on...
What I have already tried
Several breakpoints and Logs to track the data, I also tried to use the following but it ruins my existing sorting and grouping.
public App App { get; set; } //Instead of the IEnumerable<App>
There are multiple issues with your code.
First you cannot bind a <select> element to a collection of complex objects. A <select> posts back the value of its selected option (which will be an int assuming the Id property of Language is int).
Next the view in the model is FilterViewModel (and your generating form controls with name attributes based on those properties), but your posting back to a different model (App) which does not contain those properties so nothing would bind anyway.
Your adding a null label option ("Select Language") and if that were selected, it would post a null value which would cause your query to fail.
There are also some bad practices which I have noted below.
Your view model should be
public class AppsFilterVM
{
public int? Language { get; set; }
public int? Platform { get; set; }
public IEnumerable<SelectListItem> LanguageOptions { get; set; }
public IEnumerable<SelectListItem> PlatformOptions { get; set; }
...
public IEnumerable <App> Apps { get; set; }
}
Its not clear what AgeGroups and ProductCodes are for so I have omitted them in the code above, and from your comments, I have assumed that the user can filter by either Language or Platform or both
The controller code would be
public ActionResult FilterApps(AppsFilterVM model)
{
var apps = _context.Apps;
if (model.Language.HasValue)
{
apps = apps.Where(x => x.LanguageId == model.Language.Value);
}
if (model.Platform.HasValue)
{
apps = apps.Where(x => x.PlatformId == model.Platform.Value);
}
model.Apps = apps;
ConfigureViewModel(model);
return View(model);
}
private void ConfigureViewModel(AppsFilterVM model)
{
// populate the selectlists
var languages = _context.Languages;
var platforms = _context.Platforms
model.LanguageOptions = new SelectList(languages, "Id", "Name");
model.PlatformOptions = new SelectList(platforms , "Id", "Name");
}
Then in the view (note its making a GET, not a POST)
#model.AppsFilterVM
....
#using (Html.BeginForm("FilterApps", "Apps", FormMethod.Get))
{
#Html.LabelFor(m => m.Language)
#Html.DropdownListFor(m => m.Language, Model.LanguageOptions, "No filter")
#Html.ValidationMessageFor(m => m.Language)
#Html.LabelFor(m => m.Platform)
#Html.DropdownListFor(m => m.Platform, Model.PlatformOptions, "No filter")
#Html.ValidationMessageFor(m => m.Platform)
<input type="submit" value="Filter" />
}
#foreach (var group in Model.AgeGroups)
{
....
There a a few other thing you should not be doing. Your giving both <select> elements the same id attribute which is invalid html (the DropDownListFor() method already generates a unique id based on the property name).
You should not submit a form based on the change event of a <select> Not only is it unexpected behavior, if a user uses the keyboard to navigate through options (e.g. using the arrow keys, or typing a character to go to the first option starting with that letter, then the form will be immediately submitted. In addition, the user might select an option from the 2nd dropdownlist first, which would immediately post before they have a chance to select the option in the first one. Allow the user to make their selections, check them, and then submit the form when they choose to.
Your view should not contain linq queries, and your grouping and ordering should be done in the controller before you pass the model to the view. Your Apps property should in fact be a view model containing a property for the group name, and a collection property for the Apps, (similar to the view models in your previous question) so that the view is simply
#foreach(var group in Model.AgeGroups)
{
#group.Name
foreach (var app in group.Apps)
{
#app.ProductCode
#app.Name
#app.Platform
}
}
You should also consider using ajax to submit your form, which would call separate server method that returns a partial view of just the Apps, and update the DOM in the success callback, which would improve performance. For an example, refer Rendering partial view on button click in ASP.NET MVC.
I know there are a lot of similar question here but none seem quite the same as mine.
In my View:
#model LocalInformedMotionServer.Models.FeedData
#Html.DropDownList("Premise", Model.LoadUp("key"), new { #style = "width: 218px;height:35px;" })
In my controller:
public class CloudController : Controller
{
public IEnumerable<wsCommon.Premise> Premises { get; set; }
public ActionResult Feed(string key)
{
var feedData = new FeedData();
Premises= feedData.LoadUp(key);
return View(feedData);
}
}
In my Model:
public class FeedData
{
public IEnumerable<wsCommon.Premise> LoadUp(string saltKey)
{
Premises premises = new InformedBiz.Premises();
return premises.GetPremises(saltKey);
}
}
It errors because the variable:
"key"
in this call:
Model.LoadUp("key")
is being read in as'null' in my controller method.
Of course as this is all new to me I could be doing this all wrong..
ADDITIONAL:
In my CloudController Class I have this:
public class CloudController : Controller
{
public ActionResult Feed(string saltKey)
{
var feedData = new FeedData();
feedData.LoadUp(saltKey);
return View(feedData);
}
public ActionResult Index()
{
return View();
}
public ActionResult LogIn()
{
return View();
}
}
I'm not sure what your Premise class looks like, but I usually use an IEnumberable of SelectListItem for drop downs in my views. So you could do something like this:
public IEnumerable<SelectListItem> LoadUp(string saltKey)
{
Premises premises = new InformedBiz.Premises();
return premises.GetPremises(saltKey).Select(
p => new SelectListItem { Text = p.Name, Value = z.PremiseId.ToString() }
);
}
You'll also need to create a Post ActionResult method that accepts the model in your view (FeedData) as well as wrap your DropDownList control in a Html.BeginForm, to post results to the controller. Hope this makes a bit of sense.
You have not posted the properties of your FeedData model but assuming it contains a property which is typeof Premise and you want to be able to select a Premise from a collection, then using a view model that represents what you want to display/edit is the recommended approach (refer View Model Design And Use In Razor Views and What is ViewModel in MVC?)
You view model might look like
public class FeedDataVM
{
.....
[Display(Name = "Premise")]
[Required(ErrorMessage = "Please select a Premise")]
public int? SelectedPremise { get; set; }
....
public SelectList PremiseList { get; set; }
}
and in your controller (not sure what saltKey is for?)
public ActionResult Feed(string saltKey)
{
FeedDataVM model = new FeedDataVM();
IEnumerable<Premise> premises = // get the collection of premise objects from your repository
// assuming you want to display the name property of premise, but post back the key property
model.PremiseList = new SelectList(premises, "key", "name");
return View(model);
}
View
#model FeedDataVM
#using(Html.BeginForm())
{
....
#Html.LabelFor(m => m.SelectedPremise)
#Html.DropDownListFor(m => m.SelectedPremise, Model.PremiseList, "-Please select")
#Html.ValidationMessageFor(m => m.SelectedPremise)
....
<input type="submit" value="Save" />
}
and the POST method
public ActionResult Feed(FeedDataVM model)
{
// model.SelectedPremise contains the value of the selected option as defined by the key property of Premise
}
Side note: Your FeedData model contains a method to retrieve a collection of Premise which appears to be calling another service. You should avoid this type of design which makes it difficult to debug and unit test. Your controller is responsible for initializing/getting you data models and view models and for populating/mapping their properties.
I've read many articles which they state that querying should not be placed in the Controller, but I can't seem to see where else I would place it.
My Current Code:
public class AddUserViewModel
{
public UserRoleType UserRoleType { get; set; }
public IEnumerable<SelectListItem> UserRoleTypes { get; set; }
}
public ActionResult AddUser()
{
AddUserViewModel model = new AddUserViewModel()
{
UserRoleTypes = db.UserRoleTypes.Select(userRoleType => new SelectListItem
{
Value = SqlFunctions.StringConvert((double)userRoleType.UserRoleTypeID).Trim(),
Text = userRoleType.UserRoleTypeName
})
};
return View(model);
}
The View:
<li>#Html.Label("User Role")#Html.DropDownListFor(x => Model.UserRoleType.UserRoleTypeID, Model.UserRoleTypes)</li>
How do I retain the View Model and Query and exclude the User Type that should not show up?
I think that you are doing it just fine.
Any way... all you can do to remove the querying logic from controller is having a ServiceLayer where you do the query and return the result.
The MVC pattern here is used correctly... what your are lacking is the other 2 layers (BusinessLayer and DataAccessLayer)... since ASP.NET MVC is the UI Layer.
UPDATE, due to comment:
Using var userroletypes = db.UserRoleTypes.Where(u=> u.UserRoleType != 1);
is OK, it will return a list of UserRoleType that satisfy the query.
Then, just create a new SelectList object using the userroletypes collection... and asign it to the corresponding viewmodel property. Then pass that ViewModel to the View.
BTW, I never used the db.XXXX.Select() method before, not really sure what it does... I always use Where clause.
SECOND UPDATE:
A DropDownList is loaded from a SelectList that is a collection of SelectItems.
So you need to convert the collection resulting of your query to a SelectList object.
var userroletypes = new SelectList(db.UserRoleTypes.Where(u=> u.UserRoleType != 1), "idRoleType", "Name");
then you create your ViewModel
var addUserVM = new AddUserViewModel();
addUserVM.UserRoleTypes = userroletypes;
and pass addUserVM to your view:
return View(addUserVM );
Note: I'm assuming your ViewModel has a property of type SelectList... but yours is public IEnumerable<SelectListItem> UserRoleTypes { get; set; } so you could change it or adapt my answer.
I don't see anything wrong with your code other than this db instance that I suppose is some concrete EF context that you have hardcoded in the controller making it impossible to unit test in isolation. Your controller action does exactly what a common GET controller action does:
query the DAL to fetch a domain model
map the domain model to a view model
pass the view model to the view
A further improvement would be to get rid of the UserRoleType domain model type from your view model making it a real view model:
public class AddUserViewModel
{
[DisplayName("User Role")]
public string UserRoleTypeId { get; set; }
public IEnumerable<SelectListItem> UserRoleTypes { get; set; }
}
and then:
public ActionResult AddUser()
{
var model = new AddUserViewModel()
{
UserRoleTypes = db.UserRoleTypes.Select(userRoleType => new SelectListItem
{
Value = SqlFunctions.StringConvert((double)userRoleType.UserRoleTypeID).Trim(),
Text = userRoleType.UserRoleTypeName
})
};
return View(model);
}
and in the view:
#model AddUserViewModel
<li>
#Html.LabelFor(x => x.UserRoleTypeId)
#Html.DropDownListFor(x => x.UserRoleTypeId, Model.UserRoleTypes)
</li>
I tried searching and didn't find anything that fixed my problem. I have a DropDownList on a Razor view that will not show the the item that I have marked as Selected in the SelectList. Here is the controller code that populates the list:
var statuses = new SelectList(db.OrderStatuses, "ID", "Name", order.Status.ID.ToString());
ViewBag.Statuses = statuses;
return View(vm);
Here is the View code:
<div class="display-label">
Order Status</div>
<div class="editor-field">
#Html.DropDownListFor(model => model.StatusID, (SelectList)ViewBag.Statuses)
#Html.ValidationMessageFor(model => model.StatusID)
</div>
I walk through it and even in the view it has the correct SelectedValue however the DDL always shows the first item in the list regardless of the selected value. Can anyone point out what I am doing wrong to get the DDL to default to the SelectValue?
The last argument of the SelectList constructor (in which you hope to be able to pass the selected value id) is ignored because the DropDownListFor helper uses the lambda expression you passed as first argument and uses the value of the specific property.
So here's the ugly way to do that:
Model:
public class MyModel
{
public int StatusID { get; set; }
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
// TODO: obviously this comes from your DB,
// but I hate showing code on SO that people are
// not able to compile and play with because it has
// gazzilion of external dependencies
var statuses = new SelectList(
new[]
{
new { ID = 1, Name = "status 1" },
new { ID = 2, Name = "status 2" },
new { ID = 3, Name = "status 3" },
new { ID = 4, Name = "status 4" },
},
"ID",
"Name"
);
ViewBag.Statuses = statuses;
var model = new MyModel();
model.StatusID = 3; // preselect the element with ID=3 in the list
return View(model);
}
}
View:
#model MyModel
...
#Html.DropDownListFor(model => model.StatusID, (SelectList)ViewBag.Statuses)
and here's the correct way, using real view model:
Model
public class MyModel
{
public int StatusID { get; set; }
public IEnumerable<SelectListItem> Statuses { get; set; }
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
// TODO: obviously this comes from your DB,
// but I hate showing code on SO that people are
// not able to compile and play with because it has
// gazzilion of external dependencies
var statuses = new SelectList(
new[]
{
new { ID = 1, Name = "status 1" },
new { ID = 2, Name = "status 2" },
new { ID = 3, Name = "status 3" },
new { ID = 4, Name = "status 4" },
},
"ID",
"Name"
);
var model = new MyModel();
model.Statuses = statuses;
model.StatusID = 3; // preselect the element with ID=3 in the list
return View(model);
}
}
View:
#model MyModel
...
#Html.DropDownListFor(model => model.StatusID, Model.Statuses)
Make Sure that your return Selection Value is a String and not and int when you declare it in your model.
Example:
public class MyModel
{
public string StatusID { get; set; }
}
Create a view model for each view. Doing it this way you will only include what is needed on the screen. As I don't know where you are using this code, let us assume that you have a Create view to add a new order.
Create a new view model for your Create view:
public class OrderCreateViewModel
{
// Include other properties if needed, these are just for demo purposes
// This is the unique identifier of your order status,
// i.e. foreign key in your order table
public int OrderStatusId { get; set; }
// This is a list of all your order statuses populated from your order status table
public IEnumerable<OrderStatus> OrderStatuses { get; set; }
}
Order status class:
public class OrderStatus
{
public int Id { get; set; }
public string Name { get; set; }
}
In your Create view you would have the following:
#model MyProject.ViewModels.OrderCreateViewModel
#using (Html.BeginForm())
{
<table>
<tr>
<td><b>Order Status:</b></td>
<td>
#Html.DropDownListFor(x => x.OrderStatusId,
new SelectList(Model.OrderStatuses, "Id", "Name", Model.OrderStatusId),
"-- Select --"
)
#Html.ValidationMessageFor(x => x.OrderStatusId)
</td>
</tr>
</table>
<!-- Add other HTML controls if required and your submit button -->
}
Your Create action methods:
public ActionResult Create()
{
OrderCreateViewModel viewModel = new OrderCreateViewModel
{
// Here you do database call to populate your dropdown
OrderStatuses = orderStatusService.GetAllOrderStatuses()
};
return View(viewModel);
}
[HttpPost]
public ActionResult Create(OrderCreateViewModel viewModel)
{
// Check that viewModel is not null
if (!ModelState.IsValid)
{
viewModel.OrderStatuses = orderStatusService.GetAllOrderStatuses();
return View(viewModel);
}
// Mapping
// Insert order into database
// Return the view where you need to be
}
This will persist your selections when you click the submit button and is redirected back to the create view for error handling.
I hope this helps.
For me, the issue was caused by big css padding numbers ( top & bottom padding inside the dropdown field). Basically, the item was being shown but not visible because it was way down. I FIXED it by making my padding numbers smaller.
I leave this in case it helps someone else. I had a very similar problem and none of the answers helped.
I had a property in my ViewData with the same name as the selector for the lambda expression, basically as if you would've had ViewData["StatusId"] set to something.
After I changed the name of the anonymous property in the ViewData the DropDownList helper worked as expected.
Weird though.
My solution was this...
Where the current selected item is the ProjectManagerID.
View:
#Html.DropDownList("ProjectManagerID", Model.DropDownListProjectManager, new { #class = "form-control" })
Model:
public class ClsDropDownCollection
{
public List<SelectListItem> DropDownListProjectManager { get; set; }
public Guid ProjectManagerID { get; set; }
}
Generate dropdown:
public List<SelectListItem> ProjectManagerDropdown()
{
List<SelectListItem> dropDown = new List<SelectListItem>();
SelectListItem listItem = new SelectListItem();
List<ClsProjectManager> tempList = bc.GetAllProductManagers();
foreach (ClsProjectManager item in tempList)
{
listItem = new SelectListItem();
listItem.Text = item.ProjectManagerName;
listItem.Value = item.ProjectManagerID.ToString();
dropDown.Add(listItem);
}
return dropDown;
}
Please find sample code below.
public class Temp
{
public int id { get; set; }
public string valueString { get; set; }
}
Controller
public ActionResult Index()
{
// Assuming here that you have written a method which will return the list of Temp objects.
List<Temp> temps = GetList();
var tempData = new SelectList(temps, "id", "valueString",3);
ViewBag.Statuses = tempData;
return View();
}
View
#Html.DropDownListFor(model => model.id, (SelectList)ViewBag.Statuses)
#Html.ValidationMessageFor(model => model.id)
I am trying to use the Html.ListBoxFor helper to show a list box and return the selected Id. Is there a problem with the dataValueField not being a string?
If the SelectList contained in the model uses integers as the dataValueField then I get a "Value cannot be null - Parameter name: Source" exception raised when the list is rendered in the view.
If the Id is changed to a string then everything works and the selected Id is passed back to the view.
Any ideas?
Here is the controller (based on a cut down new project)
namespace Mvc2.Controllers
{
public class ViewModel
{
public int TestId { get; set; } // if this is a string it works ok
public SelectList ListData {get; set;}
}
[HandleError]
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new ViewModel();
model.TestId = 1; // code corrected after Lazarus' comment
var lst = new[] { new { Id = 1, Name = "cat" }, new { Id = 2, Name = "dog" } };
model.ListData = new SelectList(lst, "Id", "Name");
return View("TestView", model);
}
public ActionResult TestSubmit(ViewModel returnedModel)
{
int i = 99; // break here - returnedModel has correct TestId when declared as string
}
}
}
here is the View - crashes on the ListBoxFor line
<%using (Html.BeginForm("TestSubmit", "Home")) { %>
<%=Model.TestId %><br />
<%=Html.ListBoxFor(m => m.TestId, Model.ListData) %>
<br />
<input type="submit" value="Save" />
<%} %>
The expression you are passing for the selected values needs to be IEnumerable because ListBoxFor supports multiple selected items.
Answering my own question;
I am unconviced by the comments that this might be a bug which is waiting to be fixed because I get it in RC2 and in MVC 1 (I copied the code back to a project in that release).
Anyway I have implemented a work around for now which is to:-
(a) Add a dummy string version of the Id to the model (TestId)
public class ViewModel
{
public string TestId { get; set; } // dummy Id as a string
public List<DataToShow> Data { get; set; }
public SelectList ListData {get; set;}
}
(b) Display the list but retrieve the value as the dummy TestId - note that the list still dumps the data values as integers!
<%=Html.ListBoxFor(m => m.TestId, Model.ListData) %>
(c) Copy the dummy string value into its proper integer location in the action
public ActionResult TestSubmit(ViewModel returnedModel)
{
MyModel.DataId = Int32.Parse(returnedModel.TestId);
Hope this is of some Help.
This is a known issue with ASP.NET MVC 2. It should be fixed in the March release.