I'm trying to teach myself MVC. I mocked up a dummy project to reflect a real problem I have at the moment.
My Model(s):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace MvcPersistPartialView.Models {
public class RemarkModels {
public static List<RemarkTemplateModel> GetTemplateAll() {
return new List<RemarkTemplateModel>() {RemarkTemplateModel.Create(1),
RemarkTemplateModel.Create(2),
RemarkTemplateModel.Create(3)};
}
}
public class RemarkModel {
public int ID { get; set; }
public string Content { get; set; }
public int TemplateID { get; set; }
public List<RemarkTemplateModel> Templates { get; set; }
public static RemarkModel Create() {
return new RemarkModel() {
Content = "This is a dummy remark, to learn MVC.",
TemplateID = 1,
Templates = RemarkModels.GetTemplateAll()
};
}
}
public class RemarkTemplateModel {
public int ID { get; set; }
public string Name { get; set; }
public List<RemarkTemplateFieldModel> Fields { get; set; }
public static RemarkTemplateModel Create(int id) {
return new RemarkTemplateModel() {
ID = id,
Name = id.ToString(),
Fields = new List<RemarkTemplateFieldModel>() {RemarkTemplateFieldModel.Create("Label A" + id.ToString(),
id.ToString() + "..."),
RemarkTemplateFieldModel.Create("Label B" + id.ToString(),
id.ToString() + "..."),
RemarkTemplateFieldModel.Create("Label C" + id.ToString(),
id.ToString() + "...")}
};
}
}
public class RemarkTemplateFieldModel {
public string Label { get; set; }
public string Content { get; set; }
public static RemarkTemplateFieldModel Create(string label, string content) {
return new RemarkTemplateFieldModel() { Label = label,
Content = content };
}
}
}
What you see in the above models is a RemarkModel, representing a remark you can save on a person like: "This person hides issues with a big smile. Watch out for that!".
You also see a RemarkTemplateModel to accomodate a slightly complex wish of my endusers to fill in certain pieces of information like with surveys. So, this 'RemarkModel' could have template 'Testtemplate A'.
Templates consist of fields (RemarkTemplateFieldModel) which could look like: 'Testtemplate A' consists out of the fields: 'Field 'A', field 'B' and field 'C' (while ofcourse some 'Testtemplate B' could consist out of fields 'D', 'E', 'F', 'G'). I hope I made this clear. Please ask if it's not clear.
Last model is RemarkModels (the 's' at the back), that's just to retrieve all the mocked up templates quickly.
My controller:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace MvcPersistPartialView.Controllers {
public class HomeController : Controller {
public ActionResult Index() {
ViewBag.Message = "Welcome to ASP.NET MVC!";
Models.RemarkModel r = Models.RemarkModel.Create();
return View(r);
}
[HttpPost]
public ActionResult Index(Models.RemarkModel remark, Models.RemarkTemplateModel template, List<Models.RemarkTemplateFieldModel> fields) {
if (ModelState.IsValid) {
//Persist to database.
}
ViewBag.Message = "Welcome to ASP.NET MVC!";
Models.RemarkModel r = Models.RemarkModel.Create();
return View(r);
}
[AcceptVerbs("POST")]
public ActionResult TemplateChosen(string selectedTemplateID) {
Models.RemarkTemplateModel selectedTemp = (from temp in Models.RemarkModels.GetTemplateAll()
where temp.ID == Convert.ToInt32(selectedTemplateID)
select temp).FirstOrDefault();
return PartialView("RemarkTemplate", selectedTemp);
}
public ActionResult About() {
return View();
}
}
}
As you can see with parameterless 'Index' I create a RemarkModel and pass it in my strongly typed View Index.cshtml. This view looks like:
#model MvcPersistPartialView.Models.RemarkModel
#using (Html.BeginForm()) {
<fieldset>
<div class="editor-label">
#Html.LabelFor(model => model.Content)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Content)
#Html.ValidationMessageFor(model => model.Content)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Templates)
</div>
<div class="editor-field">
#Html.DropDownListFor(
m => m.TemplateID,
new SelectList(Model.Templates, "ID", "Name"),
new {
id = "templateDdl",
data_url = Url.Action("TemplateChosen", "Home")
}
)
<input type="button" value="Change template" id="template-button" />
</div>
<div id="template">
#Html.Partial("RemarkTemplate", Model.Templates.First())
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
So far so good. I've been on here before to ask help with the DropDownListFor, this works like a charm now (so thank you community). We are getting to the core of my question, thank you for your patience so far. The partial view 'RemarkTemplate' looks like:
#model MvcPersistPartialView.Models.RemarkTemplateModel
<div class="display-field">
#Html.Encode(Model.ID)
</div>
<div class="display-field">
#* #Html.Encode(Model.Name)*#
#Html.EditorFor(m => m.Name)
</div>
#foreach (MvcPersistPartialView.Models.RemarkTemplateFieldModel field in Model.Fields) {
#* <div class="editor-label">
#Html.Encode(field.Label)
</div>
<div class="editor-field">
#Html.TextBox("Content", field.Content)
</div>*#
#Html.Partial("RemarkTemplateField", field)
}
We'll get to the 2nd partial view. Because first I tried this without the partial view 'RemarkTemplateField' and only 2 parameters (remark + template) in the POST Action 'Index'. My problem is that the 'template' parameter's 'fields' property remained 'null'. I also noticed nothing was really posted about the template untill I used an 'EditorFor' for the name property of the template.
The 'remark' parameter however contained the correct template ID in it's TemplateID property, so I figured if I could atleast get the fields, the template doesn't matter. So that is when I added the 2nd partial view:
#model MvcPersistPartialView.Models.RemarkTemplateFieldModel
<div class="editor-label">
#Html.Encode(Model.Label)
</div>
<div class="editor-field">
#*#Html.TextBox("Content", Model.Content)*#
#Html.EditorFor(m => m.Content)
</div>
It al displays perfectly. Very nice. But the POSTback to the Index action keeps returning an empty 'template' (well the 'name' of the template is filled in since I made it an EditorFor which is not an option on the real website) and the 'fields' list is empty. As you can see I again changed the Textbox on Content to an EditorFor. But alas, no magic.
So my question is my dear experts, how does one get a more complex model like this bound properly in MVC? How can I get the filled in contents of the fields in the Action of my Controller, so I can ultimatly persist the remark with the correct filled in template to the database? I would be very grateful for your help. Point me to tutorials, if you don't want to spell it out, which can help me. Just give me some starters. I've been at it for a workday now...
I think what you are looking for is the binding of collections. This may help: http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx
Related
I have a form like this:
JS Code:
$(document).ready(function() {
$("#add-more").click(function() {
selectedColor = $("#select-color option:selected").val();
if (selectedColor == '')
return;
var color = ' <
div class = "form-group" >
<
label class = "col-md-2 control-label" > Color: < /label> <
div class = "col-md-5" > < label class = "control-label" > ' + selectedColor + ' < /label></div >
<
/div>
';
var sizeAndQuantity = ' <
div class = "form-group" >
<
label class = "col-md-2 control-label" > Size and Quantity: < /label> <
div class = "col-md-2" > < label class = "control-label" > S < /label><input type="text" class="form-control"></div >
<
div class = "col-md-2" > < label class = "control-label" > M < /label><input type="text" class="form-control"></div >
<
div class = "col-md-2" > < label class = "control-label" > L < /label><input type="text" class="form-control"></div >
<
div class = "col-md-2" > < label class = "control-label" > XL < /label><input type="text" class="form-control"></div >
<
/div>
';
html = color + sizeAndQuantity
$("#appendTarget").append(html)
});
});
Old Code:
Model:
namespace ProjectSem3.Areas.Admin.Models
{
public class ProductViewModel
{
public ProductGeneral ProductGeneral { get; set; }
public List<SizeColorQuantityViewModel> SizeColorQuantities { get; set; }
}
public class ProductGeneral
{
public string Product { get; set; }
public string Description { get; set; }
public string ShortDescription { get; set; }
public List<ProductCategory> Categories { get; set; }
public string SKU { get; set; }
public float Price { get; set; }
public float PromotionPrice { get; set; }
public bool Status { get; set; }
}
public class SizeColorQuantityViewModel
{
public string ColorId { get; set; }
public List<SizeAndQuantity> SizeAndQuantities { get; set; }
}
public class SizeAndQuantity
{
public string SizeId { get; set; }
public int Quantity { get; set; }
}
}
Controller:
public class ProductController : Controller
{
// GET: Admin/Product
public ActionResult Create()
{
var colors = new List<string>() { "Red", "Blue" };
var sizes = new List<string>() { "S", "M", "L", "XL" };
var categories = new ProductDao().LoadProductCategory();
var productGeneral = new ProductGeneral()
{
Categories = categories
};
var model = new ProductViewModel
{
ProductGeneral = productGeneral,
SizeColorQuantities = new List<SizeColorQuantityViewModel>()
};
foreach (var color in colors)
{
var child = new SizeColorQuantityViewModel
{
ColorId = color,
SizeAndQuantities = new List<SizeAndQuantity>()
};
model.SizeColorQuantities.Add(child);
foreach (var size in sizes)
{
child.SizeAndQuantities.Add(new SizeAndQuantity()
{
SizeId = size
});
}
}
return View(model);
}
// POST: Admin/Product
[HttpPost]
public ActionResult Create(ProductViewModel model)
{
return View();
}
}
View:
#for (var i = 0; i < Model.SizeColorQuantities.Count; i++)
{
<div class="form-group">
<label class="col-md-2 control-label">Color:</label>
<div class="col-md-2">
#Html.TextBoxFor(m => m.SizeColorQuantities[i].ColorId, new { #class = "form-control", #readonly = "readonly" })
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label">Size and Quantity:</label>
#for (var j = 0; j < Model.SizeColorQuantities[i].SizeAndQuantities.Count; j++)
{
<div class="col-md-2">
#Html.TextBoxFor(m => m.SizeColorQuantities[i].SizeAndQuantities[j].SizeId, new
{
#class = "form-control",
#style = "margin-bottom: 15px",
#readonly = "readonly"
})
#Html.TextBoxFor(m => m.SizeColorQuantities[i].SizeAndQuantities[j].Quantity, new { #class = "form-control" })
</div>
}
</div>
}
I choose a color and click Add, it'll add more item in to list. I'm newbie in ASP.NET MVC. I just know value from Razor how to pass value form
I also asked about same thing in here and received kind explain. But, it's static value which pass from controller and then is used to bind into razor.
But now, it isn't static.
Could you tell me how to bind razor item in to list to post it to controller? I would be very grateful if you give me some suggest.
Thanks for your kind helping. (bow)
You can refer this post. It works perfect for me.
http://ivanz.com/2011/06/16/editing-variable-length-reorderable-collections-in-asp-net-mvc-part-1/
I will quote it below:
The aspects I will consider are:
Dynamically adding, removing and reordering items to/from the
collection
Validation implications
Code Reusability and Refactoring implications I will assume that you are already familiar with ASP.NET MVC and basic JavaScript concepts.
Source Code
All source code is available on GitHub
The Sample
What I am going to build is a little sample where we have a user who has a list of favourite movies. It will look roughly like on the image below and will allow for adding new favourite movies, removing favourite movies and also reordering them up and down using the drag handler.
In Part 1 I look at implementing collection editing by sticking to facilities provided to us by ASP.NET MVC such as views, partial views, editor templates, model binding, model validation, etc.
Domain Model
The domain model is basically:
public class User
{
public int? Id { get; set; }
[Required]
public string Name { get; set; }
public IList<Movie> FavouriteMovies { get; set; }
}
and
public class Movie
{
[Required]
public string Title { get; set; }
public int Rating { get; set; }
}
Let’s get cracking!
An Edit View
Let’s start by creating a first-pass edit view for our Person to look like the one on the image above:
#model CollectionEditing.Models.User
#{ ViewBag.Title = "Edit My Account"; }
<h2>Edit</h2>
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<legend>My Details</legend>
#Html.HiddenFor(model => model.Id)
<div class="editor-label">
#Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Name)
#Html.ValidationMessageFor(model => model.Name)
</div>
</fieldset>
<fieldset>
<legend>My Favourite Movies</legend>
#if (Model.FavouriteMovies == null || Model.FavouriteMovies.Count == 0) {
<p>None.</p>
} else {
<ul id="movieEditor" style="list-style-type: none">
#for (int i=0; i < Model.FavouriteMovies.Count; i++) {
<li style="padding-bottom:15px">
<img src="#Url.Content("~/Content/images/draggable-icon.png")" style="cursor: move" alt=""/>
#Html.LabelFor(model => model.FavouriteMovies[i].Title)
#Html.EditorFor(model => model.FavouriteMovies[i].Title)
#Html.ValidationMessageFor(model => model.FavouriteMovies[i].Title)
#Html.LabelFor(model => model.FavouriteMovies[i].Rating)
#Html.EditorFor(model => model.FavouriteMovies[i].Rating)
#Html.ValidationMessageFor(model => model.FavouriteMovies[i].Rating)
Delete
</li>
}
</ul>
Add another
}
<script type="text/javascript">
$(function () {
$("#movieEditor").sortable();
});
</script>
</fieldset>
<p>
<input type="submit" value="Save" />
Cancel
</p>
}
he view is creating a list of editing controls for each of the movies in Person.FavouriteMovies. I am using a jQuery selector and dom function to remove a movie when the user clicks “Delete” and also a jQuery UI Sortable to make the items from the HTML list drag and droppable up and down.
With this done we immediately face the first problem: We haven’t implemented the “Add another”. Before we do that let’s consider how ASP.NET MVC model binding of collections works.
ASP.NET MVC Collection Model Binding Patterns
There are two patterns for model binding collections in ASP.NET MVC. The first one you have just seen:
#for (int i=0; i < Model.FavouriteMovies.Count; i++) {
#Html.LabelFor(model => model.FavouriteMovies[i].Title)
#Html.EditorFor(model => model.FavouriteMovies[i].Title)
#Html.ValidationMessageFor(model => model.FavouriteMovies[i].Title)
…
}
which generates similar HTML:
<label for="FavouriteMovies_0__Title">Title</label>
<input id="FavouriteMovies_0__Title" name="FavouriteMovies[0].Title" type="text" value="" />
<span class="field-validation-error">The Title field is required.</span>
This is really great for displaying collections and editing static length collections, but problematic when we want to edit variable length collections, because:
1. Indices have to be sequential (0, 1, 2, 3, …). If they aren’t ASP.NET MVC stops at the first gap. E.g. if you have item 0, 1, 3, 4 after the model binding has finished you will end up with a collection of two items only – 1 and 2 instead of four items.
2. If you were to reorder the list in the HTML ASP.NET MVC will apply the indices order not the fields order when doing model binding.
This basically means that add/remove/reorder scenarios are no go with this. It’s not impossible but it will be big big mess tracking add/remove/reorder actions and re-indexing all field attributes.
Now, someone might say – “Hey, why don’t you just implement a non-sequential collection model binder?” .
Yes, you can write the code for a non-sequential collection model binder. You will however face two major issues with that however. The first being that the IValueProvider doesn’t expose a way to iterate through all values in the BindingContext which you can workaround* by hardcoding the model binder to access the current HttpRequest Form values collection (meaning that if someone decides to submit the form via Json or query parameters your model binder won’t work) or I’ve seen one more insane workaround which checks the *BindingContext one by one from CollectionName[0] to CollectionName[Int32.MaxValue] (that’s 2 billion iterations!).
Second major issue is that once you create a sequential collection from the non-sequential indices and items and you have a validation error and you re-render the form view your ModelState will no longer match the data. An item that used to be at index X is now at index X-1 after another item before it was deleted, however the ModelState validation message and state still point to X, because this is what you submitted.
So, even a custom model binder won’t help.
Thankfully there is a second pattern, which mostly helps for what we want to achieve (even though I don’t think it was designed to solve exactly this):
<input type="hidden" name="FavouriteMovies.Index" value="indexA"/>
<input name="FavouriteMovies[indexA].Title" type="text" value="" />
<input name="FavouriteMovies[indexA].Rating" type="text" value="" />
<input type="hidden" name="FavouriteMovies.Index" value="indexB"/>
<input name="FavouriteMovies[indexB].Title" type="text" value="" />
<input name="FavouriteMovies[indexB].Rating" type="text" value="" />
Notice how we have introduced an “.Index” hidden field for each collection item. By doing that we tell ASP.NET MVC’s model binding “Hey, don’t look for a standard numeric collection index, but instead look for the custom Index value we have specified and just get me the list of items in a collection when you are done”. How does this help?
We can specify any index value we want
The index doesn’t have to be sequential and items will be put in the collection in the order they are in the HTML when submitted.
Bam! That’s solves most, but not all of our problems.
The Solution
Firstly, ASP.NET MVC doesn’t have HTML helpers to generate the “[something].Index” pattern which is major problem since it means we can’t use validation and custom editors. We can fix that by utilizing some ASP.NET templating fu. What we are going to do is move the Movie editor to a its own partial view (MovieEntryEditor.cshtml):
#model CollectionEditing.Models.Movie
<li style="padding-bottom:15px">
#using (Html.BeginCollectionItem("FavouriteMovies")) {
<img src="#Url.Content("~/Content/images/draggable-icon.png")" style="cursor: move" alt=""/>
#Html.LabelFor(model => model.Title)
#Html.EditorFor(model => model.Title)
#Html.ValidationMessageFor(model => model.Title)
#Html.LabelFor(model => model.Rating)
#Html.EditorFor(model => model.Rating)
#Html.ValidationMessageFor(model => model.Rating)
Delete
}
</li>
And update our Edit view to use it:
<ul id="movieEditor" style="list-style-type: none">
#foreach (Movie movie in Model.FavouriteMovies) {
Html.RenderPartial("MovieEntryEditor", movie);
}
</ul>
<p><a id="addAnother" href="#">Add another</a>
Notice two things – firstly the Movie partial edit view uses standard Html helpers and secondly there is a call to something custom called Html.BeginCollectionItem. *You might even ask yourself: Wait a second. This won’t work, because the partial view will produce names such as “Title” instead of “FavouriteMovies[xxx].Title”, so let me show you the source code of *Html.BeginCollectionItem:
public static IDisposable BeginCollectionItem<TModel>(this HtmlHelper<TModel> html, string collectionName)
{
string itemIndex = Guid.NewGuid().ToString();
string collectionItemName = String.Format("{0}[{1}]", collectionName, itemIndex);
TagBuilder indexField = new TagBuilder("input");
indexField.MergeAttributes(new Dictionary<string, string>() {
{ "name", String.Format("{0}.Index", collectionName) },
{ "value", itemIndex },
{ "type", "hidden" },
{ "autocomplete", "off" }
});
html.ViewContext.Writer.WriteLine(indexField.ToString(TagRenderMode.SelfClosing));
return new CollectionItemNamePrefixScope(html.ViewData.TemplateInfo, collectionItemName);
}
private class CollectionItemNamePrefixScope : IDisposable
{
private readonly TemplateInfo _templateInfo;
private readonly string _previousPrefix;
public CollectionItemNamePrefixScope(TemplateInfo templateInfo, string collectionItemName)
{
this._templateInfo = templateInfo;
_previousPrefix = templateInfo.HtmlFieldPrefix;
templateInfo.HtmlFieldPrefix = collectionItemName;
}
public void Dispose()
{
_templateInfo.HtmlFieldPrefix = _previousPrefix;
}
}
This helper does two things:
Appends a hidden Index field to the output with a random GUID value
(remember that using the .Index pattern an index can be any string)
Scopes the execution of the helper via an IDisposable and sets the
template rendering context (html helperes and display/editor
templates) to be “FavouriteMovies[GUID].”, so we end up with HTML
like this:
Title
This solves the problem of using Html field templates and basically reusing ASP.NET facilities instead of having to write html by hand, but it leads me to the second quirk that we need to address.
Let me show you the second and final problem. Disable client side validation and delete the title of e.g. “Movie 2” and click submit. Validation will fail, because Title of a movie is a required field, but while we are shown the edit form again** there are no validation messages**:
Why is that? It’s the same problem I mentioned earlier in this post. Each time we render the view we assign different names to the fields, which do not match the ones submitted and leads to a *ModelState *inconsistency. We have to figure out how to persist the name and more specifically the Index across requests. We have two options:
Add a hidden CollectionIndex field and CollectionIndex property on the Movie object to persist the FavouriteMovies.Index. This however is intrusive and suboptimal.
Instead of polluting the Movie object with an extra property be smart and in our helper Html.BeginCollectionItem reapply/reuse the submitted FavouriteMovies.Index form values.
Let’s replace in Html.BeginCollectionItem this line:
string itemIndex = Guid.New().ToString();
with:
string itemIndex = GetCollectionItemIndex(collectionIndexFieldName);
And here’ is the code for GetCollectionItemIndex:
private static string GetCollectionItemIndex(string collectionIndexFieldName)
{
Queue<string> previousIndices = (Queue<string>) HttpContext.Current.Items[collectionIndexFieldName];
if (previousIndices == null) {
HttpContext.Current.Items[collectionIndexFieldName] = previousIndices = new Queue<string>();
string previousIndicesValues = HttpContext.Current.Request[collectionIndexFieldName];
if (!String.IsNullOrWhiteSpace(previousIndicesValues)) {
foreach (string index in previousIndicesValues.Split(','))
previousIndices.Enqueue(index);
}
}
return previousIndices.Count > 0 ? previousIndices.Dequeue() : Guid.NewGuid().ToString();
}
We get all submitted values for e.g. “FavouriteMovie.Index” put them in a queue, which we store for the duration of the request. Each time we render a collection item we dequeue its old index value and if none is available we generate a new one. That way we preserve the Index across requests and can have a consistent ModelState and see validation errors and messages:
All that is left is to implement the “Add another” button functionality and we can do that easily by appending a new row to the movie editor, which we can fetch using Ajax and use our existing MovieEntryEditor.cshtml partial view like that:
public ActionResult MovieEntryRow()
{
return PartialView("MovieEntryEditor");
}
And then add the follwing “Add Another” click handler:
$("#addAnother").click(function () {
$.get('/User/MovieEntryRow', function (template) {
$("#movieEditor").append(template);
});
});
Done;
Conclusion
While not immediately obvious editing variable length reorderable collections with standard ASP.NET MVC is possible and what I like about this approach is that:
We can keep using traditional ASP.NET html helpers, editor and display templates (Html.EditorFor, etc.) with in our collection editing
We can make use of the ASP.NET MVC model validation client and server side
What I don’t like that much however is:
That we have to use an AJAX request to append a new row to the editor.
That we need to use the name of the collection in the movie editor partial view, but otherwise when doing the standalone AJAX get request the name context won’t be properly set for the partial template fields.
I would love to hear your thoughts. The sample source code is available on my GitHub
Other way: http://blog.stevensanderson.com/2008/12/22/editing-a-variable-length-list-of-items-in-aspnet-mvc/
I'm struggling to set up a ListView which allows me to add new records and to update existing ones. Loading the items for the list works like a charm. Also showing the only relevant information Id and Line works perfectly.
Two problems:
1. Adding a new item to the list: I see the ugly form which allows to set the value for Id and Line. But there is no way for me to save it.
2. Updating an existing item: I see the ugly form which allows to set the value for Id and Line. But the fields are empty. They should be pre-filled with whatever has been shown before. Again, there is no way for me to save it.
My code looks pretty similar to what is provided in Telerik's Examples Project. Unfortunately, I fail to find the minor difference...
The extremely stripped code of the ListView (still not working as intended):
#using System.Collections
#using TestcaseRepositoryAPI.Model.Domain;
#model TemplateGeneratorItem
#{
ViewBag.Title = "VORLAGE";
ViewBag.SubTitle = "erstellen";
ViewBag.ShowMenu = true;
Layout = "~/Views/Shared/_LayoutMetro.cshtml";
}
#Html.Partial("_MetroPageHeader")
<div class="demo-section">
<a class="k-button k-button-icontext k-add-button" href="#"><span class="k-icon k-add"></span>Add new record</a>
</div>
<div class="demo-section k-header">
#(Html.Kendo().ListView<TemplateGeneratorItem.Record>(Model.Records)
.Name("RecordsList")
.ClientTemplateId("recordLinesTemplate")
.DataSource(d => d
.Events(e => e.RequestStart("requestStart").RequestEnd("requestEnd"))
.Create("RecordLineCreate", "Metro")
.Read("RecordLinesRead", "Metro")
.Update("RecordLineUpdate", "Metro")
.Destroy("RecordLineDelete", "Metro")
.Model(m => m.Id(o => o.Line))
)
.Editable()
//.Events(e => e.Remove("removeRecord"))
.HtmlAttributes(new { style = "border:none;" })
.TagName("div")
)
</div>
<script type="text/x-kendo-tmpl" id="recordLinesTemplate">
<div class="k-widget" style="margin:10px auto 10px auto;">
<fieldset>
<legend>Zeile #=Line#</legend>
<div class="edit-buttons">
<a class="k-button k-button-icontext k-edit-button" href="\\#"><span class="k-icon k-edit"></span></a>
<a class="k-button k-button-icontext k-delete-button" href="\\#"><span class="k-icon k-delete"></span></a>
</div>
<dl>
<dt>ID</dt>
<dd>#:Id#</dd>
</dl>
</fieldset>
</div>
</script>
<script type="text/javascript">
function requestStart(e) {
console.log("requestStart(e)", e);
}
function requestEnd(e) {
console.log("requestEnd(e)", e);
console.log("e.type", e.type);
if (e.response) {
console.log("e.response", e.response);
}
}
function getRecList() {
return $("#RecordsList").data("kendoListView");
}
$(".k-add-button").click(function (e) {
getRecList().add();
e.preventDefault();
});
</script>
The snippet of the controller which returns the data:
public JsonResult RecordLinesRead([DataSourceRequest] DataSourceRequest request)
{
List<TemplateGeneratorItem.Record> records = GetTemplateGeneratorItemFromSession().Records;
int line = 1;
foreach (TemplateGeneratorItem.Record record in records)
{
record.Line = line++;
}
return Json(records.ToTreeDataSourceResult(request), JsonRequestBehavior.AllowGet);
}
And finally the classes of the model:
namespace TestcaseRepositoryAPI.Model.Domain
{
[DataContract]
public class TemplateGeneratorItem
{
[DataMember]
public List<Record> Records { get; set; }
public class Record
{
[DataMember]
//[ScaffoldColumn(false)]
public int Line { get; set; }
[DataMember]
public string Id { get; set; }
[DataMember]
[ScaffoldColumn(false)]
public List<RecordField> Fields { get; set; }
public Record() { }
public Record(Record record)
{
Line = record.Line;
Id = record.Id;
Fields = record.Fields;
}
}
public class RecordField
{
[DataMember]
public string Name { get; set; }
...
}
}
}
Found it. The Telerik support asked the right questions.
The problem is quite simple: There is no editor template specified! There are (at least?) two ways to resolve this issue:
1. Specify the name of the editor template (e.g. .Editable(e => e.Editable(true).TemplateName("TemplateGeneratorRecord"))) and create the TemplateGeneratorRecord.cshtml in the EditorTemplates folder.
2. Or let some magic happen by creating an editor template in the EditorTemplates folder called Record.cshtml. That's the solution used in Telerik's examples.
I've chosen the first option. Now it is working as expected.
I've got a page basically displaying an article backed by a database.
Below that article, there is a comments section. This is provided by a #Html.Action call returning a _Comments partial.
Within that _Comments partial. There is an optional _AddComment #Html.Action call that renders a _AddComment partial within it.
The _AddComment partial is backed by _AddComment controller methods for GET and POST.
[HttpPost]
[ValidateAntiForgeryToken()]
public ActionResult _AddComment(EditComment comment)
The GET method just returns an "EditComment" VM with the AssetID attached.
Whenever a comment is filled in and posted within the _AddComment view. It's controller method is called correctly, but the model isn't passed back.
If I look at the Request parameters I can see all the properties of the model being passed back correctly. However, it's not being bound into the Controllers method parameter.
I've tried specifying "Model" as the route params for the Html.Begin form. It's made no difference.
Have looked at a number of SO posts, none of which sort the issue I'm having!
Presumably the model binding is failing somewhere for some reason. But obviously without an exception I've no idea what's wrong!
View Model Code
public class EditComment
{
public Boolean HasRating { get; set; }
public int AssetID { get; set; }
public int CommentID { get; set; }
public int Rating { get; set; }
public string Comment { get; set; }
}
View Code
#model SEISMatch.UI.Models.Content.EditComment
<hr />
<h3><span class="colored">///</span> Leave a Comment</h3>
<div class="row" style="margin-top: 20px;">
#using (Html.BeginForm("_AddComment", "Content", Model, FormMethod.Post))
{
#Html.ValidationSummary(false)
#Html.AntiForgeryToken()
#Html.HiddenFor(m => m.AssetID)
#Html.HiddenFor(m => m.CommentID)
if (Model.HasRating)
{
#Html.EditorFor(m => m.Rating, "_StarRating")
}
<div class="span7">
#Html.TextAreaFor(m => m.Comment, new { #class = "span7", placeholder = "Comment", rows = "5" })
</div>
<div class="span7 center">
<button type="submit" class="btn btn-success">Post comment</button>
</div>
}
</div>
Your action parameter name is comment and class EditComment has Property Comment. Modelbinder gets confused.
Rename your action parameter and problem solved.
[HttpPost]
[ValidateAntiForgeryToken()]
public ActionResult _AddComment(EditComment model)
I'm new to MVC and new to programming (in a general sense) as well.
I'm struggling with the concept of how to post multiple database entries into my database (via my controller) based on values I'm pulling from a MultiSelectList that I've populated.
Basically, I'm trying to create a simple model that contains a State and a City, and lets the user select and save multiple City values to their account based on State/City values pulled from a separate, static database.
Here's my Model:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.ComponentModel.DataAnnotations;
namespace BidFetcher.Models
{
public class SPServiceLocation
{
public int SPServiceLocationID { get; set; }
public int SPCompanyAccountID { get; set; }
[Display(Name = "State")]
public string state { get; set; }
[Required]
[Display(Name = "Cities (select all that apply)")]
public string city { get; set; }
public virtual SPCompanyAccount SPCompanyAccount { get; set; }
}
}
Here is a snippet of my View data (including the multi-select list for city that I have no trouble populating):
<div class="editor-label">
#Html.LabelFor(model => model.state)
</div>
<div class="editor-field">
#Html.DropDownList("state", ViewBag.state as SelectList, "Select a state...", new { #class = "chzn-select", onchange = "CityChange()" })
#Html.ValidationMessageFor(model => model.state)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.city)
</div>
<div class="editor-field" id="SP_SelectCity">
#Html.ListBox("city", ViewBag.city as MultiSelectList, new { #class = "chzn-select", data_placeholder = "Select cities..." })
#Html.ValidationMessageFor(model => model.city)
</div>
<p>
<input type="submit" value="Submit" />
</p>
And here are my Create/Post Controllers:
public ActionResult Create()
{
ViewBag.sessionName = HttpContext.Session["SPCompanyName"].ToString();
var compID = HttpContext.Session["SPCompanyAccountID"].ToString();
ViewBag.companyID = compID;
ViewBag.state = new SelectList(simpleDB.simpleState, "simpleStateID", "simpleStateID");
ViewBag.city = new MultiSelectList(simpleDB.simpleCity, "cityFull", "cityFull");
return View();
}
[HttpPost]
public ActionResult Create(SPServiceLocation spservicelocation, FormCollection formValues)
{
if (ModelState.IsValid)
{
db.SPServiceLocation.Add(spservicelocation);
db.SaveChanges();
return RedirectToAction("Create", "SPServiceLocation");
}
return View(spservicelocation);
}
As you can see, I'm missing something in my [HttpPost] that'll allow me to save multiple values to the db database, but I'm not sure what exactly it is that's missing. I've seen some posts that explain this in terms of creating an IEnumerable list, but I guess I'm just not sure I need to do that since I'm already successfully populating my MultiSelectList via a database call.
Any help is greatly appreciated!
EDIT:
Based on the answers below, if I want to create multiple new database rows based on the results I collect from a MultiSelectList, I need to use Form collection to grab those results and parse them within my HttpPost:
And... I do not know how to do that. I assume something along these lines:
[HttpPost]
public ActionResult Create(SPServiceLocation spservicelocation, FormCollection formValues)
{
if (ModelState.IsValid)
{
foreach (var item in x)
{
db.SPServiceLocation.Add(spservicelocation);
db.SaveChanges();
}
return RedirectToAction("Create", "SPServiceLocation");
}
return View(spservicelocation);
}
Something along the lines of the above, where I create a collection based on my multiselectlist, break it into multiple variables, and step through my database.SaveChanges multiple times before I redirect?
Thanks!
The trick has nothing to do with your MVC code. You have to change either your Business Layer or Data Access Layer (DAL) code to read/write to 2 different databases.
I did the same project to answer one question sometime ago. Here it is: https://stackoverflow.com/a/7667172/538387
I checked the code, It's still working, you can download it and check it yourself.
When user submits the Create forms and application flows to [HttpPost] public ActionResult Create action, all of the form data live in formValues parameter.
You have to read those data from formValues (like: formValues["companyID"], build appropriate model object from them, and then update the db.
This is my model
public class AdministrationModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string EmailAddress { get; set; }
public bool IsApproved { get; set; }
}
This is my controller
public ActionResult GetTabContent(string id)
{
switch (id)
{
case "tab3":
model = GetAllUsersInfo();
viewName = "Administration";
break;
}
return View(viewName);
}
private List<AdministrationModel> GetAllUsersInfo()
{
List<AdministrationModel> userList = new List<AdministrationModel>();
foreach (MembershipUser user in Membership.GetAllUsers())
{
UserProfile userProfile = UserProfile.GetUserProfile(user.UserName);
userList.Add(new AdministrationModel { EmailAddress = user.Email, IsApproved = user.IsApproved, FirstName = userProfile.FirstName, LastName = userProfile.LastName });
}
return userList;
}
This is my View
#model List<AdminContainerModel>
#using (Html.BeginForm("Administration", "Account"))
{
<fieldset>
<div>
#foreach (AdministrationModel AM in Model)
{
<div>
<div class="colFull">#Html.DisplayFor(modelItem => AM.FirstName)</div>
<div class="colFull">#Html.DisplayFor(modelItem => AM.LastName)</div>
<div class="colFull">#Html.DisplayFor(modelItem => AM.EmailAddress)</div>
<div class="colPartial"><input type="checkbox" checked="#AM.IsApproved"/> </div>
<div class="clear"></div>
</div>
}
</div>
<input type="submit" value="Update Account" />
</fieldset>
}
When the user clicks the Update Account button it goes to the controller
[HttpPost]
public ActionResult Administration(List<AdministrationModel> model)
{
return View();
}
inside this method, model is always null. however the View that renders everything is perfect and shows what I want it to show. What am I doing wrong?
When using collections, in order to correctly process them so they are model-bound on post without any additional leg work, you need to make sure they are indexed correctly, you can do this by using a for loop, something like:
#for (int i = 0; i < Model.Count; i++)
{
#Html.HiddenFor(m => m[i].FirstName)
#Html.HiddenFor(m => m[i].LastName)
#Html.HiddenFor(m => m[i].EmailAddress)
<div>
<div class="colFull">#Html.DisplayFor(m => m[i].FirstName)</div>
<div class="colFull">#Html.DisplayFor(m => m[i].LastName)</div>
<div class="colFull">#Html.DisplayFor(m => m[i].EmailAddress)</div>
<div class="colPartial">#Html.CheckBoxFor(m => m[i].IsApproved)</div>
<div class="clear"></div>
</div>
}
That should model bind without any other code :)
Edit: Sorry I forgot, displayFors by default don't put the correct properties on for model binding, added hiddenFors for the other fields that don't have an editorFor
Edit2: Based on your other question in the comment, if it was public facing and you didn't want them to change any of the hidden for values using the dev tools, try the following:
Ok so you don't want them to change the hiddenFors, that's fine, but you will need some sort of ID so you know which client is which when the data is posted, I suggest that instead of having these in the above code:
#Html.HiddenFor(m => m[i].FirstName)
#Html.HiddenFor(m => m[i].LastName)
#Html.HiddenFor(m => m[i].EmailAddress)
Replace them with:
#Html.HiddenFor(m => m[i].ClientId)
That way you're not posting back the firstname, lastname or email address, just a reference to the actual client that is ticked, unticked.
To answer your other question in the comment about keeping track of the original values, in your controller method you can just go and get the original values from the database, then here's how you can detect which ones are different, something like:
[HttpPost]
public ActionResult Administration(List<AdministrationModel> model)
{
var originalMatches = GetAllUsersInfo();
var differences = (from o in originalMatches
join c in model on o.ClientId equals c.ClientId
where c.IsApproved != o.IsApproved).ToList()
return View();
}
Your #model directive is incorrect, it should be the fully qualified type name.
In this case:
#model TheNamespace.AdministrationModel
After your updated question.
Try using:
#Html.EditorFor(Model)
This will call a specialized Editor Template for your list.
http://blogs.msdn.com/b/nunos/archive/2010/02/08/quick-tips-about-asp-net-mvc-editor-templates.aspx
If you want to return list items with the model on submit you will need to use a for loop, not a foreach
Matty's answer will work.
If you want to avoid having to specify indexes (which wouldn't work if you had an ICollection) you can define a child template as Xander has explained.
The benefit is you still get all your strong typed intellisense in the AdminContainerModel View and MVC hooks the items back in to your List on post back for you out of the box.
Here's an example:
Main Editor template (as Xander said):
#model IEnumerable<AdminContainerModel>
#using (Html.BeginForm("Administration", "Account"))
{
<fieldset>
<div>
#Html.EditorForModel()
</div?
</fieldset>
}
AdminContainerModel Editor template(This is called for each item in your List because you've called #Html.EditorForModel:
#model AdminContainerModel
<div>
<div class="colFull">#Html.DisplayFor(modelItem => AM.FirstName)</div>
<div class="colFull">#Html.DisplayFor(modelItem => AM.LastName)</div>
<div class="colFull">#Html.DisplayFor(modelItem => AM.EmailAddress)</div>
<div class="colPartial"><input type="checkbox" checked="#AM.IsApproved"/>
</div>
<div class="clear"></div>