I have a ViewModel that I'm trying to pass to my controller which contains a dictionary. I'm able to get everything to show up correctly in the view, however when I got to post to my controller I never get the the StudentKeys dictionary to go through, however all other information goes through fine.
View
#using (Html.BeginForm("BulkEditStudentRecordsAdultEd", "Order", FormMethod.Post, new { #id = "EditStudentAdultEd", #class = "form-horizontal" }))
{
#Html.HiddenFor(m => m.AdultEdAgencyCd)
foreach (var item in Model.StudentKeys)
{
#Html.HiddenFor(m => item.Key)
#Html.HiddenFor(m => item.Value)
}
<h1>#Model.StudentKeys.Count</h1>
<div class="row inputRow">
#Html.BootstrapDropDownListFor(m => m.DistrictCd, Model.DistrictCodes, "col-md-5", labelText: "District:", htmlAttributes: new { #id = "DistrictCd" })
#Html.BootstrapDropDownListFor(m => m.SchoolCd, Model.SchoolCodes, "col-md-5", labelText: "School:", htmlAttributes: new { #id = "SchoolCd" })
</div>
<div class="col-md-offset-2">
<button id="SaveStudentBulkEdit" type="submit" class="btn btn-primary btn-sm">Save <span class="badge">0</span></button>
<button id="CancelStudentBulkEdit" type="button" class="btn btn-default btn-sm" data-dismiss="modal">Cancel</button>
</div>
}
Controller
[HttpPost]
[CustomAuthorize(DiplomaRoles.SchoolStaff, DiplomaRoles.StateAdultEdManagement, DiplomaRoles.SchoolAdmin, DiplomaRoles.StateAdmin)]
public ActionResult BulkEditStudentRecordsAdultEd(EditStudentAdultEdViewModel vm) //All other information from my view model gets through
{
var keys = vm.StudentKeys; //Empty
var DistrictCd = vm.DistrictCd; //Ok
var SchoolCd = vm.SchoolCd; //Ok
}
View Model
public class EditStudentAdultEdViewModel
{
public EditStudentAdultEdViewModel()
{
StudentKeys = new Dictionary<string, string>();
}
public string DistrictCd {get; set;}
public string SchoolCd {get; set;}
public string IdentificationNbr{ get; set; }
public int DuplicateNbr { get; set; }
public Dictionary<string, string> StudentKeys { get; set; }
public string AdultEdAgencyCd { get; set; }
}
MVC already puts everything that was submitted into a dictionary for you.
public ActionResult BulkEditStudentRecordsAdultEd(FormCollection vm)
{
// to access an item, do this: vm["inputName"]
}
If you want the default modelbinder to bind for you into a different dictionary, you can do so by having inputs with correct names:
<input type="hidden" name="vm[KeyString]" value="ValueString" />
Related
I have a form in which user can select which shipping methods they want to support for they product that they are selling, e.g. first class letter, second class letter, parcel, etc. I only give users a collection of possible shipping methods, they declare how much each one will cost, so if someone wants to sell a toaster in a parcel, they will charge less than for a set of dumbbells.
My ProductViewModel:
public int Id { get; set; }
public ICollection<SelectedShippingMethodViewModel> SelectedShippingMethods { get; set; }
And SelectedShippingMethodViewModel:
public class SelectedShippingMethodViewModel
{
public string Name { get; set; }
public decimal Price { get; set; }
}
In my form I create a section with possible options like this:
<h3>Add new product</h3>
<hr />
#using (Html.BeginForm("AddNew", "ProductCreator", null, FormMethod.Post, new { #class = "form-horizontal", role = "form" }))
{
#Html.AntiForgeryToken()
<div class="form-group">
<label class="col-sm-2 control-label">Shipping methods</label>
<div class="col-sm-10">
#foreach (ShippingMethod shippingMethod in ViewBag?.ShippingMethods)
{
<div class="row">
<div class="col-md-3">
// I don't know what should be here
#Html.CheckBox("SelectedShippingMethods", false)
#shippingMethod.Name
</div>
<div class="col-md-2">
// I don't know what should be here
#Html.TextBox("SelectedShippingMethods.Price")
</div>
</div>
}
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-default">Add product</button>
</div>
</div>
}
I have a database table with every possible shipping method that I acquire like this:
[HttpGet]
public async Task<ActionResult> AddNew()
{
ViewBag.ShippingMethods = await _shippingService.GetAllShippingMethodsAsync();
return View();
}
The problem is if checkbox is selected I have to bind Price and Name for each individual SelectedShippingMethodViewModel and I have no idea how to make it work.
Your view models are incorrect. To allow users to select the shipping methods they want and add a price, that view model needs to be
public class ShippingMethodViewModel
{
public string Name { get; set; }
public decimal Price { get; set; }
public bool IsSelected { get; set; } // your checkbox binds to this property
}
and the ProductViewModel should be
public class ProductViewModel
{
public int Id { get; set; }
....
public List<ShippingMethodViewModel> ShippingMethods { get; set; }
}
Then in the GET method, initialize your ProductViewModel and populate the ShippingMethods based on all available ShippingMethods, for example
var shippingMethods = await _shippingService.GetAllShippingMethodsAsync()
ProductViewModel model = new ProductViewModel
{
....
ShippingMethods = shippingMethods.Select(x => new ShippingMethodViewModel
{
Name = x.Name
}).ToList()
};
return View(model);
and in the view, use a for loop or EditorTemplate for typeof ShippingMethodViewModel to correctly generate your form controls
#for (int i = 0; i < Model.ShippingMethods.Count; i++)
{
#Html.LabelFor(m => m.ShippingMethods[i].IsSelected, Model[0].ShippingMethods.Name)
#Html.CheckBoxFor(m => m.ShippingMethods[i].IsSelected)
#Html.LabelFor(m => m.ShippingMethods[i].Price)
#Html.TextBoxFor(m => m.ShippingMethods[i].Price)
#Html.HiddenFor(m => m.ShippingMethods[i].Name) // if you want this to be submitted as well
}
Then in the POST method
public ActionResult AddNew(ProductViewModel model)
{
// Get the selected Shipping Methods and the associated price
var selectedMethods = model.ShippingMethods.Where(x => x.Selected);
Model.cs
A campaign can have multiple images, that's why IEnumerable<int> ImageIdList.
public class Campaign
{
public int Id { get; set; }
public int CreatedBy { get; set; }
public int UpdatedBy { get; set; }
public IEnumerable<int> ImageIdList { get; set; }
}
View.cshtml
I want to download all the images related to a campaign, based on the ImageIdList, that's why I need to post all these ImageIds when a particular Campaign is checked and download button is clicked.
#model Campaign
#{
Layout = "....";
var assets = Model.AssetsInCampaign.ToList();
}
#using (Html.BeginForm("action-method", "controller", FormMethod.Post))
{
<div class="btnSubmit">
<input type="submit" value="Download Asset(s)" />
</div>
#foreach(var i in assets)
{
<div class="s_checkcol">
<input type="checkbox" name="ids" />
#foreach (var imageId in i.Where(c => c.AssetId == doc.FileDataId).SelectMany(c => c.ImageIdList))
{
<input type="hidden" name="ids" value=#(imageId)>
}
</div>
}
}
Controller.cs
public ActionResult DownloadFiles(IEnumerable<int> ids)
{
// code
}
NOTE: Only a part of code(where I'm facing the problem) is provided here. Its a DB first approach and in no way I can alter that (ORDERS).
I tried the above, but all of the ids are posted to the controller no matter how many checkboxes are selected.
Question: How should I bind the IEnumerable<int> ImageIdList property to a checkbox in View.cs and post the data to Controller.cs so that only the ids of selected checkboxes are posted?
This is a nice practice... it will work and Iam working with such a
manner (Iam sure that it will work very well) but one thing you have to be very carefull while coding this, little bit
complicated
Iam taking this effort not only for as an answer to this particular question.
Its for all stackoverflow users. Because i never found the below method anyware in stackoverflow.
I get this method by a long search. You people can use this.
It will help you to avoid for loops to bind the Checkboxlist
Its the best good for re-usability (need a single line (max: 20-25 chars to bind a CheckBoxList in Razor))
CheckBoxListItem.cs
create a New Class CheckBoxListItem //you can use any other names
public class CheckBoxListItem
{
public int ID { get; set; }
public string Display { get; set; }
public bool IsChecked { get; set; }
}
MyModel.cs
This is modelclass
public class MyModel
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public IEnumerable<CheckBoxListItem> ChkList { get; set; }
}
HomeController.cs
This is controller
public ActionResult Index()
{
var model = new MyModel(){
Id = 0,
Name = "Your Name",
ChkList = dbContext.myTable.Select(x => new CheckBoxListItem { ID = x.MyTableFieldID, Display = x.MyTableFieldName, IsChecked = true })
//If you need only int part, then just avoid to bind data on Display field
};
return View(model);
}
[HttpPost]
public ActionResult Index(MyModel myModel) //Your model object on PostAction
{
IEnumerable<CheckBoxListItem> ChkList = myModel.ChkList;
// Here is your answer, You can see all your check box items (both checked and unchecked) in ChkList, it will shows all your checked items are true and non-checked items are false in IsChecked field
}
Here you have to give more patiance
Goto the Folder View>Shared>EditorTemplates and RightClick Add>View... and Create a new View with the same name CheckBoxListItem.cshtml
CheckBoxListItem.cshtml
#model Project.Models.CheckBoxListItem
<div class="">
#Html.HiddenFor(x => x.ID)
<div class="">
#Html.CheckBoxFor(x => x.IsChecked)
</div>
#Html.LabelFor(x => x.IsChecked, Model.Display, new { #class = "" })
</div>
Create your View
Index.cshtml
#model #model Project.Models.MyModel
<div class="form-group">
#Html.LabelFor(model => model.Id, htmlAttributes: new { #class = "" })
<div class="col-md-10">
#Html.EditorFor(model => model.Id, new { htmlAttributes = new { #class = "" } })
#Html.ValidationMessageFor(model => model.Id, "", new { #class = "" })
</div>
</div>
#Html.EditorFor(model => model.ChkList) //This only one line of code is enough to bind a checkBoxList in future
<input type="submit" value="Create" class="" />
You will get all these in your post action
I have the following ViewModel:
public class ActivityReportViewModel
{
public Dictionary<int, List<string>> Periods { get; set; }
public List<Project> Projects { get; set; }
public List<Templates> Templates { get; set; }
public DateTime TimePeriod { get; set; }
}
public class Project
{
public string Customer { get; set; }
public string ProjectNumber { get; set; }
public string ProjectDescription { get; set; }
public bool IsSelected { get; set; }
public int TemplateId { get; set; }
public bool XLSX { get; set; }
public bool PDF { get; set; }
}
I fill this ViewModel in my controller and then send it to my Create view, which works fine and the values of the Projects property are all there. However, when I postback the data to the server, the values are gone. I tried supplying HiddenFields to all properties of each Project to no avail. Here's my relevant view markup:
<div>
#Html.LabelFor(model => model.Projects, htmlAttributes: new { #class = "ms-Label" })
<ul class="ms-List" style="list-style:none;">
#for (int x = 0; x < Model.Projects.Count; x++)
{
<li class="ms-ListItem">
<span class="ms-ListItem-primaryText">#Model.Projects[x].ProjectDescription</span>
<span class="ms-ListItem-secondaryText">#Model.Projects[x].Customer</span>
<span class="ms-ListItem-tertiaryText">#Model.Projects[x].ProjectNumber</span>
#*<div class="ms-ListItem-selectionTarget js-toggleSelection"></div>*#
#Html.HiddenFor(m => Model.Projects[x].IsSelected)
#Html.HiddenFor(m => Model.Projects[x].ProjectDescription)
#Html.HiddenFor(m => Model.Projects[x].Customer)
#Html.HiddenFor(m => Model.Projects[x].ProjectNumber)
#Html.HiddenFor(m => Model.Projects[x].XLSX)
#Html.HiddenFor(m => Model.Projects[x].PDF)
<div class="ms-Dropdown">
<i class="ms-Dropdown-caretDown ms-Icon ms-Icon--caretDown"></i>
#Html.DropDownListFor(m => m.Projects[x].TemplateId, new SelectList(Model.Templates, "Id", "Name"), new { #class = "ms-Dropdown-select" })
</div>
<div class="ms-ChoiceField">
<input id="excel+#Model.Projects[x].ProjectNumber" class="ms-ChoiceField-input" value="#Model.Projects[x].XLSX" type="checkbox">
<label for="excel+#Model.Projects[x].ProjectNumber" class="ms-ChoiceField-field"><span class="ms-Label is-required">Excel</span></label>
</div>
<div class="ms-ChoiceField">
<input id="pdf+#Model.Projects[x].ProjectNumber" class="ms-ChoiceField-input" value="#Model.Projects[x].PDF" type="checkbox">
<label for="pdf+#Model.Projects[x].ProjectNumber" class="ms-ChoiceField-field"><span class="ms-Label is-required">PDF</span></label>
</div>
</li>
}
</ul>
<div>
#Html.ValidationMessageFor(model => model.Projects, "", new { #class = "text-danger" })
</div>
</div>
EDIT:
Here's my POST action method:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(ActivityReportViewModel report)
{
using (AppContainer _db = new AppContainer())
{
if (ModelState.IsValid)
{
_db.SaveChanges();
return RedirectToAction("Index");
}
return PartialView(report);
}
}
The DefaultViewModel Binder uses the HTML attribute name to determine which property to bind back to on the server. I cannot see name attribute specified on your input element. Please specify name attribute on the elements you wish to post back to the server with the property of the view model.
Specifiy name attribute as below. Notice I have added name attribute with value as the property of your view model
<input id="excel+#Model.Projects[x].ProjectNumber" class="ms-ChoiceField-input" name="#Model.Projects[x].ProjectNumber" value="#Model.Projects[x].XLSX" type="checkbox">
My main entity is the Recipe which contains a collection of Ingredient items as follows:
public class Recipe {
[Key]
public virtual int RecipeId { get; set; }
public string RecipeName { get; set; }
...
public virtual ApplicationUser LastModifiedBy { get; set; }
public virtual IList<Ingredient> Ingredients { get; set; }
}
public class Ingredient {
public virtual int IngredientId { get; set; }
[Display(Name = "Name")]
public string IngredientName { get; set; }
....
public virtual IList<Recipe> Recipes { get; set; }
}
Which is fine. Then my controller and view for creating a new Recipe are as follows:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "stuff to include")] Recipe recipe)
{
IList<int> ingredientIds = (ModelState.Values.ElementAt(1).Value.AttemptedValue).Split(',').Select(int.Parse).ToList(); //[1,2,3,4,5]
foreach (int id in ingredientIds) {
Ingredient ing = db.Ingredients.Where(i => i.IngredientId == id).FirstOrDefault() as Ingredient;
recipe.Ingredients.Add(ing);
}
db.Recipes.Add(recipe);
db.SaveChanges();
return RedirectToAction("Index");
ViewBag.Ingredients = new MultiSelectList(db.Ingredients,
"IngredientId", "IngredientName", string.Empty);
ViewBag.CreatedById = new SelectList(db.Users, "Id", "Email", recipe.CreatedById);
return View(recipe);
}
And the view:
#for (Int16 i = 0; i < 5; i++) {
<div class="form-group">
#Html.LabelFor(model => model.Ingredients, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownList("Ingredients", null, htmlAttributes: new { #class = "form-control" })
</div>
</div>
}
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="button" value="Add Ingredients" class="btn btn-default" />
</div>
</div>
So this sets ModelState.Values.ElementAt(1).Value.AttemptedValue = "1,3,5,4,5" where this is a list of id numbers. I know I can come in before the if (ModelState.IsValid) and iterate through the above and place it into recipe.Ingredients which is fine except...
It feels just so un ASP.NET MVC like, as if there's no way they could have thought of so much and not thought of this scenario? Am I missing something here? The ingredients list will be too long to make a multi select list any use.
You are creating arbitrary dropdownlists that all have the same id (invalid html) and name attribute that has no relationship to your model and wont bind on post back. You first need to create view models that represent what you want to display.
public class RecipeVM
{
[Required]
public string Name { get; set; }
[Display(Name = Ingredient)]
[Required]
public List<int?> SelectedIngredients { get; set; }
public SelectList IngredientList { get; set; }
}
Then in the controller
public ActionResult Create()
{
RecipeVM model = new RecipeVM();
// add 5 'null' ingredients for binding
model.SelectedIngredients = new List<int?>() { null, null, null, null, null };
ConfigureViewModel(model);
return View(model);
}
[HttpPost]
public ActionResult Create(RecipeVM model)
{
if (!ModelState.IsValid)
{
ConfigureViewModel(model);
return View(model);
}
// Initialize new instance of your data model
// Map properties from view model to data model
// Add values for user, create date etc
// Save and redirect
}
private void ConfigureViewModel(RecipeVM model)
{
model.IngredientList = new SelectList(db.Ingredients, "IngredientId", "IngredientName");
}
View
#model RecipeVM
#using (Html.BeginForm())
{
#Html.LabelFor(m => m.Name)
#Html.TextBoxFor(m => m.Name)
#Html.ValidationMessageFor(m => m.Name)
for (int i = 0; i < Model.SelectedIngredients.Count; i++)
{
#Html.LabelFor(m => m.SelectedIngredients[i])
#Html.DropDownListFor(m => m.SelectedIngredients[i], Model.IngredientList, "-Please select-")
}
}
Note this is based on your current implementation of creating 5 dropdowns to select 5 ingredients. In reality you will want to dynamically add ingredients (start with none). The answers here and here give you a few options to consider.
I have a Model that contains a List of a custom type.
I want the data from this type to be passed back in when a model is submitted as a HttpPost call the the controller.
However, it does not seem to do what I want. I've got where I am so far by following Passing IEnumerable or list Model to Controller using HttpPost but I'm having a problem.
My controller method:
[HttpPost]
public ActionResult UpdateStock(int id, ProductModel model)
{
return View("UpdateStock", model);
}
Now, the View is like this (trimmed):
#using (Html.BeginForm())
{
<div>
<p>
<input type="submit" value="Save" />
</p>
#Html.HiddenFor(m => m.ProductNo)
<div class = "title">
#Html.LabelFor(m => m.ProductName)
#Html.EditorFor(m => m.ProductName)
</div>
#for ( int i = 0; i < Model.Stock.Count; i++ )
{
var item = Model.Stock[i];
<div class="editor-field">
<input type="text" name="Model.Stock[#i].Key"
value="#item.Key" />
</div>
<div class="editor-field">
<input type="text" name="Model.Stock[#i].Value"
value="#item.Value" />
</div>
}
}
My problem is, that it seems the #Html.EditorFor() and <input type=.../> tags don't seem to play well with each other. If I have it like above, then the ProductNo and other properties using #Html methods won't be passed through to the model.
Any advice much appreciated.
I would simply use editor templates:
Model:
public class ProductModel
{
public string ProductNo { get; set; }
public string ProductName { get; set; }
public IEnumerable<Stock> Stocks { get; set; }
}
public class Stock
{
public string Key { get; set; }
public string Value { get; set; }
}
Controller:
public class HomeController: Controller
{
public ActionResult Index()
{
var model = new ProductModel
{
ProductNo = "123",
ProductName = "p name",
Stocks = new[]
{
new Stock { Key = "key1", Value = "value1" },
new Stock { Key = "key2", Value = "value2" },
}
};
return View(model);
}
[HttpPost]
public ActionResult Index(ProductModel model)
{
...
}
}
View:
#model ProductModel
#using (Html.BeginForm())
{
<p>
<input type="submit" value="Save" />
</p>
#Html.HiddenFor(m => m.ProductNo)
<div class = "title">
#Html.LabelFor(m => m.ProductName)
#Html.EditorFor(m => m.ProductName)
</div>
#Html.EditorFor(x => x.Stocks)
}
and then you define a custom editor template for the Stock type (~/Views/Shared/EditorTemplates/Stock.cshtml):
#model Stock
<div class="editor-field">
#Html.EditorFor(x => x.Key)
</div>
<div class="editor-field">
#Html.EditorFor(x => x.Value)
</div>