I'm building a questionnaire. The questionnaire have multiple sections, each section has multiple questions, and each question can have one to many answers. Each question can be a different type (radio buttons, checkbox, text...).
I put my tables in the model and loop through the sections table to display sections, loop through questions to display questions, loop through answerOptions to populate answers:
<fieldset>
<legend>Fields</legend>
<%foreach (var s in Model.Sections)
{ %>
<h3><%=s.SCTN_TXT %></h3>
<% var QuestsInSect = Model.GetQuestionsBySectionID(s.SCTN_ID);%>
<%foreach (var q in QuestsInSect){%>
<h4><%=q.QSTN_TXT %><%=q.QSTN_ID.ToString() %></h4>
<% var answers = Model.GetAnswerOptionByQuestionID(q.QSTN_ID); %>
<%if (q.QSTN_TYP_ID>= 3)
{%>
<%:Html.TextBox(q.QSTN_ID.ToString())%>
<%}
else if (q.QSTN_TYP_ID == 1)
{ %>
<%var answerOptions = Model.GetDropDownListAnswerOptionByQuestionID(q.QSTN_ID);%>
<%:Html.DropDownList(q.QSTN_ID.ToString(), answerOptions)%>
<%}
else
{ %>
<% foreach (var ao in answers)
{ %>
<br />
<%:Html.CheckBox(q.QSTN_ID.ToString())%>
<%=ao.ANS_VAL%>
<% }
}
}
} %>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
In my controller I loop through collection.Allkeys to figure out the answer for each question:
[HttpPost]
public ActionResult Create(FormCollection collection)
{
try
{
// TODO: Add insert logic here
List<ASSMNT_RESP> arList = new List<ASSMNT_RESP>();
foreach (string key in collection.AllKeys)
{
QSTN q = _model.GetQuestionByQuestionID(int.Parse(key));
IEnumerable<ANS_OPTN> aos = _model.GetAnswerOptionByQuestionID(int.Parse(key));
ASSMNT_RESP ar = new ASSMNT_RESP();
ar.QSTN_ID = int.Parse(key);
ar.ASSMNT_ID = 1;
if (q.QSTN_TYP_ID == 1)//dropdown
{
//do something
}
else if (q.QSTN_TYP_ID == 2)//checkboxlist
{
//do something
}
else
{
//do something
}
//_model.AddAssessmentResponse(ar);
System.Diagnostics.Trace.WriteLine(key + "---"+ collection[key]);
}
//_model.Save();
return RedirectToAction("Index");
}
catch
{
return View();
}
}
It works, but I just don't think it's a very good design. It seems like I have too much logic in the view. I would like to move the logic in the view and controller to the model. Can you recommend an easier/cleaner way to do this?
Thanks.
I'm not an expert on MVC, but I think you'd get a lot of benefit out of using a strongly-typed view based on a custom model class that you build. The properties exposed by such a model can be nested (i.e., the top-level model can consist of properties each of which are also custom classes). Conceptually, something like:
public class MyTopLevelModel
{
public MySubModel1 SubModel1 { get; set; }
public MySubModel2 SubModel2 { get; set; }
}
public class MySubModel1
{
public string AProperty { get; set; }
public int AnotherProperty { get; set; }
}
You can include collections in the class definitions, too. And then you can decorate the individual properties with with attributes specifying whether a particular property is required, a range of permissible values, etc.
It's a big subject, though, and this only scratches the surface. FWIW, I've gotten a LOT out of Steven Sanderson's Pro ASP.NET MVC2 Framework book.
Make your viewmodel more explicit.
public class ViewModel
{
public IList<SectionViewModel> Sections {get;set;}
}
public class SectionViewModel
{
public IList<QuestionViewModel> Questions {get;set;}
}
public class QuestionViewModel
{
public IList<AnswerViewModel> Answers {get;set;}
}
In your view you can then do something like this (I'm using razor):
#foreach(var section in Model.Sections)
{
<h3>#section.Name</h3>
foreach(var question in section.Questions)
{
<h4>#question.Name</h4>
foreach(var question in section.Questions)
{
#Html.EditorFor(x=> question.Answers)
}
}
}
Then create an EditorTemplate for your AnswerViewModel.
Related
I would like to using two times #model to get data from another part of my website is it possible? Because now I have got error but if I have only this first #model everything working correct.
Look -> MVC 3 - Exception Details: System.InvalidOperationException
Error 2 'SportsStore.Entities.Kategorie' does not contain a definition for 'Opis' and no extension method 'Opis' accepting a first argument of type 'SportsStore.Entities.Kategorie' could be found (are you missing a using directive or an assembly reference?) c:\Users\Rafal\Desktop\MVC ksiązka\moj projekt\sklep\SportsStore.WebUI\Views\Product\List.cshtml 16 4 SportsStore.WebUI
#model IEnumerable<SportsStore.Entities.Towar>
#model IEnumerable<SportsStore.Entities.Kategorie>
#{
ViewBag.Title = "List";
}
<h2>List</h2>
#foreach (var p in Model)
{
<div class="item">
<h3>#p.Nazwa</h3>
#p.Opis
<h4>#p.Cena.ToString("c")</h4>
</div>
}
You only can have one Model per View. But you can use another object to declarate the model:
public class SomeViewModel
{
public IEnumerable<Towar> Towars;
public IEnumerable<Category> Categories;
public SomeViewModel(IEnumerable<Towar> towars, IEnumerable<Category> categories) {
Towars = towars;
Categories = categories;
}
}
And then use it in your view like this:
#model SportsStore.Entities.SomeViewModel
#foreach (var item in Model.Towars)
{
<div class="item">
<h3>#p.Nazwa</h3>
#p.Opis
<h4>#p.Cena.ToString("c")</h4>
</div>
}
#foreach (var item in Model.Categories) {
#item.Name #* or what you need down here *#
}
I would also recommend you to use english names in MVC. It's more clear to read and understand ;).
I think this would be a case to create a ViewModel (to combine the two entities you have) and then base a View off that ViewModel.
It is best to create a view model to represent your data. Your view model must only contain what you need to display on your view. Anything that is not used you can remove, no point of it being there. Your view model can look like this:
using SportsStore.Entities;
public class YourViewModel
{
public IEnumerable<Towar> Towars { get; set; }
public IEnumerable<Kategorie> Categories { get; set; } // I assume this is categories
}
Lets say that you have to use this view model in a create view then your create action can look something like this:
public class YourController : Controller
{
private readonly ITowarRepository towarRepository;
private readonly ICategoryRepository categoryRepository;
public YourController(ITowarRepository towarRepository, ICategoryRepository categoryRepository)
{
this.towarRepository = towarRepository;
this.categoryRepository = categoryRepository;
}
public ActionResult Create()
{
YourViewModel viewModel = new YourViewModel
{
Towars = towarRepository.GetAll(),
Categories = categoryRepository.GetAll()
};
return View(viewModel);
}
}
And then in your view:
#model YourProject.DomainModel.ViewModels.YourViewModel
#foreach (var towar in Model.Towars)
{
// Do whatever
}
#foreach (var category in Model.Categories)
{
// Do whatever
}
I have an ASP.NET MVC 3 (Razor) website, and a (simplified) model called Review:
public class Review
{
public int ReviewId { get; set; }
public bool RecommendationOne
{
// hook property - gets/set values in the ICollection
}
public bool RecommendationTwo { // etc }
public ICollection<Recommendation> Recommendations { get; set; }
}
Recommendation is as follows:
public class Recommendation
{
public byte RecommendationTypeId
}
I also have an enum called RecommendationType, which i use to map the above recommendation to. (based on RecommendationTypeId).
So to summarize - a single Review has many Recommendations, and each of those Recommendations map to a particular enum type, i expose hook properties to simplify model-binding/code.
So, onto the View:
#Html.EditorFor(model => model.Recommendations, "Recommendations")
Pretty simple.
Now, for the editor template, i want to display a checkbox for each possible RecommendationType (enum), and if the model has that recommendation (e.g on edit view), i check the box.
Here's what i have:
#model IEnumerable<xxxx.DomainModel.Core.Posts.Recommendation>
#using xxxx.DomainModel.Core.Posts;
#{
Layout = null;
}
<table>
#foreach (var rec in Enum.GetValues(typeof(RecommendationType)).Cast<RecommendationType>())
{
<tr>
<td>
#* If review contains this recommendation, check the box *#
#if (Model != null && Model.Any(x => x.RecommendationTypeId == (byte)rec))
{
#* How do i create a (checked) checkbox here? *#
}
else
{
#* How do i created a checkbox here? *#
}
#rec.ToDescription()
</td>
</tr>
}
</table>
As the comments suggest - i don't know how to use #Html.CheckBoxFor. Usually that takes an expression based on the model, but i'm how sure how to bind to the hook property based on the currently looped enum value. E.g it needs to dynamically do #Html.CheckBoxFor(x => x.RecommendationOne), #Html.CheckBoxFor(x => x.RecommendationTwo), etc.
The current solution i have (which works), involves manually constructing the <input> (including hidden fields).
But as i'm just getting the hang of editor templates, hoping someone with experience can point me in a "strongly-typed" direction.
Or is there a nicer way (HTML Helper) i can do this?
I would start by introducing a proper view model for the scenario:
public enum RecommendationType { One, Two, Three }
public class ReviewViewModel
{
public IEnumerable<RecommendationViewModel> Recommendations { get; set; }
}
public class RecommendationViewModel
{
public RecommendationType RecommendationType { get; set; }
public bool IsChecked { get; set; }
}
Then the controller:
public class HomeController : Controller
{
public ActionResult Index()
{
// TODO: query the repository to fetch your model
// and use AutoMapper to map between it and the
// corresponding view model so that you have a true/false
// for each enum value
var model = new ReviewViewModel
{
Recommendations = new[]
{
new RecommendationViewModel {
RecommendationType = RecommendationType.One,
IsChecked = false
},
new RecommendationViewModel {
RecommendationType = RecommendationType.Two,
IsChecked = true
},
new RecommendationViewModel {
RecommendationType = RecommendationType.Three,
IsChecked = true
},
}
};
return View(model);
}
[HttpPost]
public ActionResult Index(ReviewViewModel model)
{
// Here you will get for each enum value the corresponding
// checked value
// TODO: Use AutoMapper to map back to your model and persist
// using a repository
return RedirectToAction("Success");
}
}
and the corresponding view (~/Views/Home/Index.cshtml):
#model YourAppName.Models.ReviewViewModel
#{
ViewBag.Title = "Index";
}
#using (Html.BeginForm())
{
#Html.EditorFor(model => model.Recommendations)
<input type="submit" value="Go" />
}
and finally the editor template (~/Views/Home/EditorTemplates/RecommendationViewModel.cshtml)
#model YourAppName.Models.RecommendationViewModel
<div>
#Html.HiddenFor(x => x.RecommendationType)
#Model.RecommendationType
#Html.CheckBoxFor(x => x.IsChecked)
</div>
Now the view code is cleaned as it should. No ifs, no loops, no LINQ, no reflection, this is the responsibility of the controller/mapper layer. So every time you find yourself writing some advanced C# logic in your view I would recommend you rethinking your view models and adapt them as necessary. That's what view models are intended for: to be as close as possible to the view logic.
Is is possible to apply an attribute to a collection, and then detect this when iterating through the collection members using ViewData.ModelMetadata.Properties ?
I would like to apply an attribute to the collection to specify whether items in the collection should be displayed in their entirety or not. I want to then detect this in the Object.ascx (that deals with the display of objects of unknown type) to decide what level of detail to display.
(Please see brad wilson's post for background on this generic templating approach)
For Example:
public class Parent
{
[SomeAttributeIWantToDetect]
public IList<Child> Children{ get; set; }
}
public class Child
{
public string Name { get; set; }
public string Details { get; set; }
}
object.ascx:
(Note, this code is from the ASP.NET MVC team, not mine)
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<% if (Model == null) { %>
<%= ViewData.ModelMetadata.NullDisplayText %>
<% } else { %>
<table cellpadding="0" cellspacing="0" border="0">
<% foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => pm.ShowForDisplay && !ViewData.TemplateInfo.Visited(pm))) { %>
<% if (prop.HideSurroundingHtml) { %>
<%= Html.Display(prop.PropertyName) %>
<% } else { %>
<tr>
<td>
<div class="display-label" style="text-align: right;">
<%= prop.GetDisplayName() %>
</div>
</td>
<td>
<div class="display-field">
<!-- *********** HERE ***************-->
<% if (prop.AdditionalValues.ContainsKey(SomeAttributeIWantToDetectAttribute))
//Do something else.....
else
%>
<%= Html.Display(prop.PropertyName) %>
<% } %>
</div>
</td>
</tr>
<% } %>
<% } %>
</table>
<% } %>
This is not MVC, it's closer to classic ASP templates
Choose which camp you want to be in, and stay there
To use MVC you need to make a ViewModel which expresses the Model in terms of a particular render destination
Your ViewModel should be built using just the logic commands from above. i.e.
if (Model == null)
{
x = ViewData.ModelMetadata.NullDisplayText
}
else
{
foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => pm.ShowForDisplay && !ViewData.TemplateInfo.Visited(pm)))
{
if (prop.HideSurroundingHtml)
{
x.Items1.Add(prop.PropertyName));
}
else
{
prop.GetDisplayName()
if (prop.AdditionalValues.ContainsKey(SomeAttributeIWantToDetectAttribute))
{
x.Items2.Add( { Text = prop.zzz, Highlight = true} ));
}
else
{
x.Items2.Add( { Text = prop.PropertyName } ));
}
}
}
}
The code above is obviously wrong, I am just try to show that you should take the complex code and logic and use it to build a ViewModel, it should never be in the view
The ViewModel has a simple construct relating to the rendering technology you are using (like html attributes etc)
The view should just contain simple iterators and layout selectors, fed from the ViewModel, never the actual Model
#UpTheCreek, you're coming across as awfully confrontational for wanting people to help you, but I'll give my 2 cents anyway.
My understanding is that you want to be able to have a child collection on a model. On the child model you're going to be including some [ScaffoldColumn("false")] attributes, but you also want to be able to put an attribute on the parent model that would cause the renderer to ignore the ScaffoldColumn attributes and just show everything. If my understanding is correct, I think you are going about this the wrong way. You should be creating separate view models for cases where you want different properties to be shown.
Also, I'm note quite clear if this is your intent because in your code sample you would only be hiding the input field and not the label itself. Perhaps you are trying to put some sort of notice that the field is hidden?
You've linked http://bradwilson.typepad.com/blog/2009/10/aspnet-mvc-2-templates-part-4-custom-object-templates.html several times with the comment "I can't use ViewModels because this is generic templating".
I don't understand why you believe this. TFD and Ryan have it exactly right. Create two different ViewModels to wrap your model, and put the ScaffoldColumn attributes on your ViewModel (or better, omit those fields entirely).
Object.ascx then detects the attribute (or of course the presence or absence of the field) on your ViewModel and displays (or doesn't) the field appropriately.
In fact, the author of the post you linked suggests doing exactly that:-
http://bradwilson.typepad.com/blog/2009/10/aspnet-mvc-2-templates-part-4-custom-object-templates.html#comment-6a00e54fbd8c4988340120a6396c7a970b
"Personally, my recommendation for
people who want strict SoC (like I do)
is to use ViewModels and only place
the annotations on the view model.
There are other issues with directly
model binding to things like LINQ to
SQL or LINQ to Entities (for example,
if you're not careful, you can destroy
your associations or inadvertently let
a bad guy bind data into something
that wasn't originally shown in the
editor), so I generally always
recommend view models anyway."
So:-
public class Parent
{
public IList<Child> Children{ get; set; }
}
public class Child
{
public String Name { get; set; }
public String Details { get; set; }
}
// Pass this one to your "Admin" view.
public class ParentAdminViewModel
{
private Parent _parent;
public ParentAdminViewModel(Parent parent) { this._parent = parent; }
public IEnumerable<Child> Children
{
get
{
return _parent.Children.Select(x => new ChildAdminViewModel(x));
}
}
}
public class ChildAdminViewModel
{
private Child _child;
public ChildAdminViewModel(Child child) { this._child = child; }
public String Name { get { return _child.Name; } }
public String Details { get { return _child.Details; } }
}
// Pass this one to your "User" view.
public class ParentUserViewModel
{
private Parent _parent;
public ParentUserViewModel(Parent parent) { this._parent = parent; }
public IEnumerable<Child> Children
{
get
{
return _parent.Children.Select(x => new ChildUserViewModel(x));
}
}
}
public class ChildUserViewModel
{
private Child _child;
public ChildAdminViewModel(Child child) { this._child = child; }
public String Name { get { return _child.Name; } }
// ChildUserViewModel doesn't have a Details property,
// so Object.ascx won't render a field for it.
}
Obviously you'll need to wire up setters as well if you want to edit.
Could you not use reflection like so: http://msdn.microsoft.com/en-us/library/z919e8tw.aspx ?
I am trying to validate a form in MVC.
I add custom errors to model state and get it as invalid when form is submitted. When the view is displayed it doesn’t show validation messages nor validation summary. Can anyone please let me know what am I doing wrong or point me in the right direction if there is any other way of validating?
Edit This is ASP.NET MVC 1. Here's the code:
Following is the entity
namespace DCS.DAL.Entities
{
public class Group : IDataErrorInfo
{
public int GroupId { get; set; }
public string GroupName { get ; set; }
public string AboutText { get; set; }
public string LogoURL { get; set; }
public string FriendlyURL { get; set; }
public bool ExcludeFromFT { get; set; }
public ContactInfo ContactInfo { get; set; }
public string Error { get { return string.Empty; } }
public string this[string propName]
{
get
{
if ((propName == "GroupName") && string.IsNullOrEmpty(GroupName))
return "Please enter Group Name";
return null;
}
}
}
}
Following is the view
<%= Html.ValidationSummary("Please correct following details") %>
<% using (Html.BeginForm()) {%>
<div id="divError" Style="display:none;">
errors
<%
foreach (KeyValuePair<string, ModelState> keyValuePair in ViewData.ModelState)
{
foreach (ModelError modelError in keyValuePair.Value.Errors)
{
%>
<% Response.Write(modelError.ErrorMessage); %>
<%
}
}
%>
</div>
<fieldset>
<table>
<tr>
<td>
<label for="GroupName">Group Name:</label>
</td>
<td>
<%= Html.TextBox("GroupName", Model.GroupName) %>
<%= Html.ValidationMessage("GroupName","group") %>
</td>
Foreach loop is for testing, it does gets into the for loop but doesn’t response.write error message nor validation summary nor validation message.
Following is the controller
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult EditGroup(Group group, FormCollection collection)
{
//Group group = new Group();
bool success = false;
try
{
var contactInfo = new ContactInfo
{
ContactName = collection["ContactName"],
Email = collection["Email"],
Fax = collection["Fax"],
HeadOfficeAddress = collection["HeadOfficeAddress"],
Freephone = collection["Freephone"],
Telephone = collection["Telephone"],
Website = collection["Website"]
};
group.ContactInfo = contactInfo;
group.GroupName = collection["GroupName"];
if(string.IsNullOrEmpty(group.GroupName))
{
ModelState.AddModelError("GroupName", "Please enter group name");
}
if (!ModelState.IsValid)
{
success = groupRepository.InsertUpdateGroup(group);
return View(group);
}
}
catch
{
}
//return Json(success);
return View(group);
}
It does go into the if(!Modelstate.isvalid) loop but it doesn’t display error.
Edit 2 I can see in the Text Visualiser that the Validation Summary does have the error message, it wont display on screen though.
Thanks
You could decorate your model properties with data annotation attributes allowing you to perform some validation logic. Here's a simplified example:
Model:
public class Group
{
[Required]
public string GroupName { get; set; }
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
return View(new Group());
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Index(Group group)
{
// Remark: You don't need the FormCollection as argument to this action,
// leave the default model binder do the job - it will also work
// for the ContactInfo property as long as you name your text fields
// appropriately. For example Html.TextBox("ContactInfo.Email", Model.ContactInfo.Email)
return View(group);
}
}
View:
<% using (Html.BeginForm()) { %>
<label for="GroupName">Group Name:</label>
<%= Html.TextBox("GroupName", Model.GroupName) %>
<%= Html.ValidationMessage("GroupName", "group") %>
<input type="submit" value="Post" />
<% } %>
It's up to you to decide whether Data Annotations are sufficient for your case, but bear in mind that if you need to perform more advanced validation scenarios you might take a look at third party frameworks like FluentValidation and xVal.
This problem has been driving me crazy for several hours now...
In my domain, I have 2 entities that are related to each other Sku and Item. Each sku can have many items.
public class Sku
{
private readonly EntitySet<Item> items;
public Sku()
{
items = new EntitySet<Item>(AttachItems, DetachItems);
}
public int SkuId { get; set; }
public string LongDescription { get; set; }
public EntitySet<Item> Items
{
get { return items; }
set{ items.Assign(value);}
}
private void AttachItems(Item entity)
{
entity.Sku = this;
}
private static void DetachItems(Item entity)
{
entity.Sku = null;
}
}
public class Item
{
public Sku Sku { get; set; }
public int ItemId { get; set; }
public string Category { get; set; }
public string Description { get; set; }
}
I am building a page that will allow the end-user to update some fields on the sku and some fields on each item at the same time.
<% using (Html.BeginForm("Save", "Merchant", FormMethod.Post,
new { enctype = "multipart/form-data" })) { %>
<fieldset>
<legend>Sku</legend>
<p><label for="SkuId">SkuId:</label>
<%= Html.TextBox("SkuId", Model.SkuId,
new{#readonly="readonly",onfocus="this.blur();"}) %></p>
<p><label for="LongDescription">LongDescription:</label>
<%= Html.TextBox("LongDescription", Model.LongDescription) %></p>
</fieldset>
<% for (int i = 0; i < Model.Items.Count; i++) { %>
<fieldset>
<legend>Item</legend>
<p><label for="ItemId">ItemId:</label>
<%= Html.TextBox(string.Format("items[{0}].{1}", i, "ItemId"),
Model.Items[i].ItemId,
new { #readonly = "readonly", onfocus = "this.blur();" })%></p>
<p><label for="Category">Category:</label>
<%= Html.TextBox(string.Format("items[{0}].{1}", i, "Category"),
Model.Items[i].Category)%></p>
<p><label for="Description">Description:</label>
<%= Html.TextBox(string.Format("items[{0}].{1}", i, "Description"),
Model.Items[i].Description)%></p>
</fieldset>
<%} // for-loop %>
<p><input type="submit" value="Save" /></p>
<%} // form %>
I have some controller code that works by accepting both a Sku and an EntitySet of Item and then assigning the Items to the Sku.
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Save(Sku sku, EntitySet<Item> items)
{
if (sku != null)
{
if (items != null)
{
sku.Items.Assign(items);
}
}
// save Sku to repository ...
// return Details view ...
}
This works, however I have noticed that it makes two trips through the DefaultModelBinder for each Item in addition to one trip for the Sku. When the Sku is bound, the setter for Items is called, and the binder even passes in a hydrated Items collection with the correct values. However, after the call to items.Assign, Items.Count is 0. This is why I have to re-assign the items in the controller code. I was expecting the items to be transferred over to the Items collection by the binder. This should eliminate the extra trip per item, since the items parameter on my controller method could be removed. Why isn’t this working?
You might need to create a custom model binder for this?
Hopefully, I am understanding you problem correctly...
Rather than defining your Save action with 2 parameters, have you tried just defining it with a single parameter, of type Sku?
You would then want to redefine the item HTML controls similar to the following example...
<%=
Html.TextBox
(
string.Format("sku.Items[{0}].{1}", i, "ItemId"),
Model.Items[i].ItemId,
new { #readonly = "readonly", onfocus = "this.blur();" }
)
%>
This way, you're populating the items directly in the Sku object itself.
Another potential solution would be to add an additional field to your Item class, such as...
Int32 SkuId { get; set; }
This way, you could define an additional hidden field for each item in your view that would be auto-bound to the SkuId of each item back at the controller.
<%=
Html.Hidden
(
string.Format("sku.Items[{0}].{1}", i, "SkuId"),
Model.Items[i].SkuId
)
%>
You could then just update your items collection independently of your Sku object. Regardless of which way you go, the two collections have to explicitly tell Linq to SQL to update the sku and items anyhow.
You could also define your own binder class, but that's probably more work than it's worth in this case. Just follow the ASP.NET MVC conventions, and I think you should be able to find something that will work without feeling like it's a hack.
I had a similar issue where EntitySets weren't bound properly in my ViewModel.
What I did was to create another property called Mvc[YourEntitySetPropertyName], as a generic list, that wrapped around the private fields of the EntitySet:
public List<InvoiceLineItem> MvcInvoiceLineItemList
{
get { return _invoiceLineItemList.ToList(); }
set { _invoiceLineItemList.AddRange(value); }
}
I then used that instead of the EntitySet property on my view markup.
There will be no need to pass the items in your controller method signature- just pass the Sku at that point:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Save(Sku sku)
{
if (sku != null)
{
// save Sku to repository ...
// return Details view ...
}
}