I have a view with a table that displays my model items. I've extracted the relevant portions of my view:
#model System.Collections.Generic.IEnumerable<Provision>
#using (Html.BeginForm("SaveAndSend", "Provision", FormMethod.Post))
{
if (Model != null && Model.Any())
{
<table class="table table-striped table-hover table-bordered table-condensed">
<tr>
...
// other column headers
...
<th>
#Html.DisplayNameFor(model => model.IncludeProvision)
</th>
...
// other column headers
...
</tr>
#foreach (var item in Model)
{
<tr>
...
// other columns
...
<td>
#Html.CheckBoxFor(modelItem => item.IncludeProvision)
</td>
...
// other columns
...
</tr>
}
</table>
<button id="save" class="btn btn-success" type="submit">Save + Send</button>
}
...
}
This works fine and the checkbox values are displayed correctly in the view depending on the boolean value of the IncludeProvision field for the given model item.
As per Andrew Orlov's answer, I've modified the view and controller and the SaveAndSend() controller method is now:
[HttpPost]
public ActionResult SaveAndSend(List<Provision> provisions)
{
if (ModelState.IsValid)
{
// perform all the save and send functions
_provisionHelper.SaveAndSend(provisions);
}
return RedirectToAction("Index");
}
However, at this point the passed in model object is null.
Including the Provision model object for completeness:
namespace
{
public partial class Provision
{
...
// other fields
...
public bool IncludeProvision { get; set; }
}
}
My question is, what is the best way to grab the checked/unchecked value from each checkbox and update the session IncludeProvision field for each model item when the 'SaveAndSend' button is clicked?
You cannot use a foreach loop to generate form controls for properties in a collection. It creates duplicate name attributes (in your case name="item.IncludeProvision") which have no relationship to your model and duplicate id attributes which is invalid html. Use either a for loop (you models needs to be IList<Provision>
for(int i = 0; i < Model.Count; i++)
{
<tr>
<td>....</td>
<td>#Html.CheckBoxFor(m => m[i].IncludeProvision)<td>
</tr>
}
or create an EditorTemplate for typeof Provision. In /Views/Shared/EditorTemplates/Provision.cshtml (note the name of the template must match the name of the type)
#model Provision
<tr>
<td>....</td>
<td>#Html.CheckBoxFor(m => m.IncludeProvision)<td>
</tr>
and in the main view (the model can be IEnumerable<Provision>)
<table>
#Html.EditorFor(m => m)
</table>
As #mattytommo said in comments, you should post your model to controller. It can be done with putting your checkbox inside a form. After clicking on button "Save and exit" all data from inputs inside this form will be serialized and sent to your controller where you can perform manipulations with session variables and so on. After that you can redirect wherever you like.
Model
public class YourModel
{
...
public bool IncludeProvision { get; set; }
...
}
View
#model YourModel
...
#using (Html.BeginForm("SaveAndSend", "Test", FormMethod.Post))
{
...
#Html.CheckBoxFor(model => model.IncludeProvision)
...
<button type="submit">Save and send</button>
}
...
Controller
public class TestController : Controller
{
...
[HttpPost]
public ActionResult SaveAndSend(YourModel model)
{
if (ModelState.IsValid)
{
// Some magic with your data
return RedirectToAction(...);
}
return View(model); // As an example
}
...
}
Related
I send from controller to view a list of objects, viewmodel is the object with some properties and pagedList, that need to be presented on page. And by pressing the button, this list need to be exported as file, that is, it need to go back to the controller and be processed there.
Model:
public class ProductsList : ListViewModel<Product>
{
public ProductsList(string prefix) : base(prefix){ }
public ProductsList(PagedList<Product> products)
{
List = products;
}
public int? ProductTypeFilter {get;set; }
public string ProductTypeFilterName {get; set;}
public string FilterBy { get; set; }
}
ListViewModel just contain PagedList.
My controller
[HttpPost]
public FileResult SaveAsFile(PagedList<Product> viewmodel)
{
...
}
And my view
#model MyProject.ViewModels.ProductsList
if (Model.List.Count > 0)
{
<table id="products_table">
<colgroup>
<col class="productType"/>
</colgroup>
<thead>
<tr>
<th >
Product type
</th>
</tr>
</thead>
<tbody>
#{ var i = 0; }
#foreach (var item in Model.List)
{
<tr>
<td onclick="window.location='#Url.Action("Details", new {id = item.Id})'">
<p>
#item.Type
</p>
</td>
}
</tr>
i++;
}
</tbody>
</table>
}
<form asp-action="SaveAsFile" enctype="multipart/form-data" method="post">
#Html.HiddenFor(m => list);
<input type="submit" value="Save as File"/>
</form>
I already have tried add to controller params tags [FromForm], [FromBody] (actually all available tags).
In view tried with hidden field in form, without it just with submit; put form on partial view; other forms: ajax, Html.ActionLink("Save as File", "SaveAsFile", new {Model}).
On debug mod Model.List has 21 items (but it can has more, like 2000 items), but when I press the button, viewmodel is creating newly.
Problem: viewmodel is creating newly and i cannot get back my full viewmodel to controller
I will be grateful for any help :)
You can set your ViewModel data in a Session variable when you send the data to your View from Controller method:
In order to setup your Session, you can follow this S.O answer
Once your Session is setup, then you can put your ViewModel in it like:
HttpContext.Session.SetObjectAsJson("ProductsList", productslist);
And then retrieve it in your POST method like this:
[HttpPost]
public FileResult SaveAsFile(PagedList<Product> viewmodel)
{
//Get your viewmodel here
var list = HttpContext.Session.GetObjectFromJson<ProductsList>("ProductsList");
}
You can also serialize your ViewModel and then send it your Controller method without using form:
Create an ActionLink:
#Html.ActionLink("Submit", "SaveAsFile", "Home", new { jsonModel= Json.Encode(Model.list) }, null)
And your Controller method:
public FileResult SaveAsFile(string jsonModel)
{
var serializer= new DataContractJsonSerializer(typeof(Model.Product));
var yourmodel= (Product)serializer.ReadObject(GenerateStreamFromString(jsonModel));
}
I have to create one view of invoice. I have many models (tables) and I want to display all data from multiple models in one view of invoice. I created one empty (without model) view and put into a partial views. One of Partial View return one view with one model but this solution return me exception... This is my work:
This is my action in controller:
public ActionResult Customer()
{
var data = db.Customer;
return PartialView("Index", data);
}
public ActionResult Invoice()
{
var data = db.Invoice;
return PartialView("Index", data);
}
public ActionResult Dealer()
{
var data = db.Dealer;
return PartialView("Index", data);
}
public ActionResult Paid()
{
var data = dane.Paid;
return PartialView("Index", data);
}
public ActionResult Products()
{
var data = dane.Products;
return PartialView("Index", data);
}
This is one of partial view:
#model IEnumerable<Invoice_v1._0.Models.Products>
<table class="table">
<tr>
<th>
#Html.DisplayNameFor(model => model.Name)
</th>
<th>
#Html.DisplayNameFor(model => model.Price)
</th>
<th>
#Html.DisplayNameFor(model => model.Amount)
</th>
<th></th>
</tr>
#foreach (var item in Model) {
<tr>
<td>
#Html.DisplayFor(modelItem => item.Name)
</td>
<td>
#Html.DisplayFor(modelItem => item.Price)
</td>
<td>
#Html.DisplayFor(modelItem => item.Amount)
</td>
</tr>
}
</table>
This is my "Index" view with Partial Views:
#Html.Partial("_Customer")
#Html.Partial("_Invoice")
#Html.Partial("_Dealer")
#Html.Partial("_Paid")
#Html.Partial("_Products")
How can I fix them?
If you insist on having a single view, then you can make rendering of partial views conditional:
if (Model != null && Model is Customer)
Html.Partial("_Customer", Model as Customer)
, and so on.
On a related note, since all the models are exclusive (you never have more than one object), I don't see the point of having one Index view. You already have one (partial) view per class, so why not just use them as regular views?
Update
If you choose to return separate view from each of the action methods, then you can do that by specifying the name of each view. Refer to this question for details: How to return ActionResult with specific View (not the controller name)
In your particular case, that would mean:
public ActionResult Customer()
{
return View("_Customer", db.Customer);
}
public ActionResult Invoice()
{
return View("_Invoice", db.Invoice);
}
public ActionResult Dealer()
{
return View("_Dealer", db.Dealer);
}
public ActionResult Paid()
{
return View("_Paid", db.Paid);
}
public ActionResult Products()
{
return View("_Products", db.Products);
}
Make sure that each of the views expects a strongly typed model. That will make it easy to implement the views.
While your approach may work, you will have less or no control over the data (if you want to let's assume join or use any of the parent data). In such situations what I do is create a ViewModel with more than one objects as part of it. For example
public class InvoiceViewModel
{
public InvoiceHead Header { get;set; }
public IList<InvoiceDetail> Details { get; set; }
}
So now I can populate this View Model before going into my view.
public ActionResult Invoice(int id)
{
InvoiceViewModel viewModel = new InvoiceViewModel();
viewModel.Header = db.InvoiceHead.SingleOrDefault(i => i.ID == id);
if(viewModel.Header != null)
{
viewModel.Details = db.Details.Where(d => d.Inv_id == id).ToList();
}
return View(viewModel);
}
Now I can design my view with either Partial views or one whole view for my invoice data.
I will put as model the IEnumerable or IList
if I use partial for both it will be
#model db.Objects.InvoiceHead // for the header
#model IEnumerable // for the details
in html code will be like that
#Html.Partial("~/Views/Customers/_AttachFileList.cshtml")
use your own folder path. hope works
I want to do something similar to what is explained in this question: I want to edit a list of data.
The difference is that the base is not a list.
(I'm using VS 2013, so it is not old stuff.)
My view model:
public class SampleViewModel
{
// ... other properties for editing ...
// The list property
public List<SampleListItemViewModel> ItemList { get; set; }
}
public class SampleListItemViewModel
{
// For display only. It has an ID field to identify the row.
public MyEntity Item { get; set; }
// I want to modify this!
public bool IsChecked { get; set; }
}
My attempted View:
#model My.Namespace.SampleViewModel
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<!-- ... normal editing, MVC generated ... -->
<!-- BEGIN LIST EDIT -->
<table>
<tr>
<th>
Is Applicable
</th>
<!-- ... -->
</tr>
#foreach (var doc in Model.ItemList)
{
#Html.HiddenFor(modelItem => doc.Document.CRMDocumentId)
<tr>
<td>
#Html.EditorFor(modelItem => doc.IsChecked)
<!-- #Html.ValidationMessageFor(modelItem => doc.IsChecked) -->
</td>
<!-- ... other non-editable display fields, e.g. name ... -->
</tr>
}
<!-- END LIST EDIT -->
<!-- ... -->
}
When I create the view it shows everything as I want it, but when I click "Create" the item list is null.
EDIT - More Info
Controller:
public ActionResult Create(int? id)
{
var item = // ...populate...
// I confirmed that ItemList has values.
return View(item);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(SampleViewModel item)
{
// This is null
var list = item.ItemList;
// ignoring all else for now
return View(item);
}
To bind complex objects, we need to provide an index for each item, rather than relying on the order of items. This ensures we can unambiguously match up the submitted properties with the correct object.
Replace the foreach loop with for loop:
#for (int i=0; i<Model.ItemList.Count; i++)
{
#Html.HiddenFor(modelItem => modelItem.ItemList[i].Document.CRMDocumentId)
<tr>
<td>
#Html.EditorFor(modelItem => modelItem.ItemList[i].IsChecked)
#Html.ValidationMessageFor(modelItem => modelItem.ItemList[i].IsChecked)
</td>
</tr>
}
NOTE:
Note that the index must be an unbroken sequence of integers starting at 0 and increasing by 1 for each element
Also for every Property in the list which you don't want user to edit like for example UserId etc add an #Html.HiddenFor(..) for that property otherwise it will posted null to the server and if hidden field created for them they will not be NULL.
For further details you can see HERE
Also refer Model Binding with List
I have got this problem that I am having a difficulty to solve. I am creating a page where the user will be presented with a list of items (Product Types). Each item will have a dropdown list next to it so that the user can make appropriate selection to create a mapping. After making selection then the user submits the form, and the value will be written to the database.
The problem is that when it is submitted, I am not getting any values back. Specifically, 'Mappings' is empty in the model that is returned by the POST action. The GET action works fine. The following is the essence of what I have written:
Model:
public class ProductTypeMappingViewModel
{
//this is empty in the POST object
public List<ProductTypeMapping> Mappings { get; set; }
public ProductTypeMappingViewModel()
{
Mappings = new List<ProductTypeMapping>();
}
public ProductTypeMappingViewModel(string db)
{
//use this to populate 'Mappings' for GET action
//works fine
}
public void UpdateDB()
{
//to be called on the object
//returned from POST action
foreach(var mapping in Mappings)
{
//Mappings is always empty after POST
//Suppose to add to db
}
}
}
public class ProductTypeMapping
{
public string ProductTypeName { get; set; }
public int SelectedStandardProductTypeKey { get; set; }
public SelectList StandardProductTypes { get; set; }
public ProductTypeMapping()
{
StandardProductTypes = new SelectList(new List<SelectListItem>());
}
public int GetSelectedProductTypeKey() { //return selected key}
public string GetSelectedProductTypeName() { //return selected name}
}
View:
#model CorporateM10.Models.ProductTypeMappingViewModel
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
#Html.ValidationSummary(true)
<table class="table">
#foreach (var dept in Model.Mappings)
{
<tr>
<td>
#Html.DisplayFor(model => dept.ProductTypeName, new { })
</td>
<td>
#Html.DropDownListFor(model => dept.SelectedStandardProductTypeKey, dept.StandardProductTypes, "(Select Department)", new { })
</td>
</tr>
}
</table>
<div>
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
}
Any insight will be greatly appreciated.
foreach here causes select element in final HTML to have incorrect name attribute. Thus nothing is posted to the server. Replace this with for loop:
<table class="table">
#for (int i=0; i<Model.Mappings.Count; i++)
{
<tr>
<td>
#Html.DisplayFor(model => model.Mappings[i].ProductTypeName, new { })
</td>
<td>
#Html.DropDownListFor(model => model.Mappings[i].SelectedStandardProductTypeKey, model.Mappings[i].StandardProductTypes, "(Select Department)", new { })
</td>
</tr>
}
</table>
As #Andrei said the problem relies on the name attribute.
But to add a little bit to his answer, here's the parameter names in the request that the default model binder expects for your case.
Mappings[0].SelectedStandardProductTypeKey
Mappings[1].SelectedStandardProductTypeKey
Mappings[2].SelectedStandardProductTypeKey
...
Without any breaks in the numbering, i.e.:
Mappings[0].SelectedStandardProductTypeKey
Mappings[2].SelectedStandardProductTypeKey
Won't work because of the missing Mapping[1]...
When you use the dropdown helper like this:
#Html.DropDownListFor(model => dept.SelectedStandardProductTypeKey, dept.StandardProductTypes, "(Select Department)", new { })
It generates an input with name="SelectedStandardProductTypeKey" (you need it to be Mappings[0].SelectedStandardProductTypeKey)
If you use a for loop and use the dropdown helper like this:
#Html.DropDownListFor(model => model.Mappings[i].SelectedStandardProductTypeKey
You'll get the input with the correct name.
Any parameter in the request for which the model binder cannot find a property in the model, it will ignore, that's why the Mappings property is null in your case.
Here are two great resource that explain all this (and that provide alternative ways to represent collections that might be useful if you can't a the for loop to generate a numbered index without breaks):
http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/
http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx
I have created my own custom ValidationAttribute:
public class UrlValidationAttribute : ValidationAttribute
{
public UrlValidationAttribute() {}
public override bool IsValid(object value)
{
if (value == null)
return true;
var text = value as string;
Uri uri;
return (!string.IsNullOrWhiteSpace(text) &&
Uri.TryCreate(text, UriKind.Absolute, out uri));
}
}
I am using that on one of my models and it works perfectly. However, now I am attempting to use it on a view model:
public class DeviceAttribute
{
public DeviceAttribute(int id, attributeDefinition, String url)
{
ID = id;
Url = url;
}
public int ID { get; set; }
[UrlValidation]
public String Url { get; set; }
}
The view model is used in the partial view like this:
#model List<ICMDB.Models.DeviceAttribute>
<table class="editor-table">
#foreach (var attribute in Model)
{
<tr>
#Html.HiddenFor(a => attribute.ID)
<td class="editor-label">
#Html.LabelFor(a => attribute.Url)
</td>
<td class="editor-field">
#Html.TextBoxFor(a => attribute.Url)
#Html.ValidationMessageFor(a => attribute.Url)
</td>
</tr>
}
</table>
For some unknown reason, while the constructor for UrlValidationAttribute fires, the IsValid function doesn't fire. Any ideas?
Edit: On further investigation, it seems this is happening because the DeviceAttribute view model is actually the view model for a partial. The full page is passed a different view model that contains the list of DeviceAttribute view models. So when my controller action is called, the full page view model is constructed and its values filled, but no DeviceAttribute view models are constructed, hence why no validation is run.
I would recommend you using editor templates instead of writing foreach loops. I suppose that your main view model looks something like this:
public class MyViewModel
{
public List<DeviceAttribute> Devices { get; set; }
...
}
Now in your main view:
#model MyViewModel
#using (Html.BeginForm())
{
<table class="editor-table">
#Html.EditorFor(x => x.Devices)
</table>
<input type="submit" value="OK" />
}
and in the corresponding editor template (~/Views/Shared/EditorTemplates/DeviceAttribute.cshtml):
#model DeviceAttribute
<tr>
#Html.HiddenFor(x => x.ID)
<td class="editor-label">
#Html.LabelFor(x => x.Url)
</td>
<td class="editor-field">
#Html.TextBoxFor(x => x.Url)
#Html.ValidationMessageFor(x => x.Url)
</td>
</tr>
And your POST action takes the view model back:
[HttpPost]
public ActionResult Index(MyViewModel model)
{
...
}
Now the default model binder will successfully bind all values in the view model and kick validation.
Here's a nice blog post about templates.