I have read Scott Hanselman's blog post:
http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx
I have then managed to replicate it for relatively simple examples where a
Model object contains a collection of simple objects. This when passed to the
view is represented in the view with HTML input field as follows:
<%= Html.TextBox("object.Collection[index].PropertyName")%>
This maps back to recreate the object including the collection Properties
when the form is posted back.
I am however having difficulty in getting the mapping to work in a more
complex example:
<% foreach (var contact in Model.Venue.VenueDetail.ContactLink.ContactDatas)
{%>
<p>
<label
for="venue.VenueDetail.ContactLink.ContactDatas[<%=Model.Venue.VenueDetail.ContactLink.ContactDatas.IndexOf(contact)%>].Data">Indoor or Outdoor:</label>
<%=
Html.TextArea("contact["+Model.Venue.VenueDetail.ContactLink.ContactDatas.IndexOf(contact)+"].Data")%>
<%=
Html.ValidationMessage("venue.VenueDetail.ContactLink.ContactDatas["+Model.Venue.VenueDetail.ContactLink.ContactDatas.IndexOf(contact)+"].Data", "*")%>
</p>
<%} %>
This appears to create the correct html, i.e. the index starts at 0, and
follows sequentially. (I have checked many times that this is the correct
path to the object/property (intellisense provides the following path:
Model.Venue.VenueDetail.ContactLink.ContactDatas[0].Data).
When submitting the form it passes back the following in the post:
[reduced for
brevity].....venue.VenueDetail.ContactLink.ContactDatas%5B0%5D.Data=fred&venue.VenueDetail.ContactLink.ContactDatas%5B1%5D.Data=wilma
However, whilst the ContactDatas object is created, it is not populated with
any ContactData objects.
Should the default model mapper map to models 3 deep, and if so is there any
obvious error in what I have stated above.
If not, then how should I best provide mappings for my ContactData objects?
Thanks,
Richard
type of URL. You will have to deconstruct your hierarchy a bit to get here,
but linking back with a huge query string will likely end up with a very
kludgy and buggy application.
Hope this helps.
--
Gregory A. Beamer
MVP; MCP: +I, SE, SD, DBA
*************************************************
| Think outside the box! |
*************************************************
"RichB" <richsa...@community.nospam> wrote in message
news:773AB148-9814-4F45...@microsoft.com...
I'm sure that my model could be a little simpler, although I would still
have Contact Data as a collection and it is complicated a little because of
the linqtosql mapping. I guess that my question boils down to whether the
defaultModelBindershould be able to map to a model of this complexity or
whether I need to do some work to simplify it or if there is a more
appropriate way to perform the mapping.
Thanks,
Richard
"Cowboy (Gregory A. Beamer)" <NoSpamM...@comcast.netNoSpamM> wrote in
message news:OJJd$Y91JH...@TK2MSFTNGP03.phx.gbl...
>I'm sure that my model could be a little simpler, although I would still
>have Contact Data as a collection and it is complicated a little because
of
the linqtosql mapping. I guess that my question boils down to whether the
>defaultModelBindershould be able to map to a model of this complexity or
>whether I need to do some work to simplify it or if there is a more
>appropriate way to perform the mapping.
Hi Richard,
I think the model updating should work. According to your code the problem
is probably on this line:
<%=
Html.TextArea("contact["+Model.Venue.VenueDetail.ContactLink.ContactDatas.In
dexOf(contact)+"].Data")%>
Here the "contact" is the local variable:
<% foreach (var contact in Model.Venue.VenueDetail.ContactLink.ContactDatas)
However, to get the mapping work we should specify the name of <input> in
this way:
ModelClassname.CollectionPropertyName[index].PropertyName.....
I've wrote a sample that shows how to do that. Please check out the
following code (create a new ASP.NET MVC Web Application named
MvcApplication3 and paste the following code into it):
Model (create a new class in the Models folder):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace MvcApplication3.Models
{
public class MyData
{
public List<MyCustomer> Customers { get; set; }
}
public class MyCustomer {
public TestClassDepth1 Depth1 { get; set; }
}
public class TestClassDepth1 {
public TestClassDepth2 Depth2{get;set;}
}
public class TestClassDepth2
{
public TestClassDepth3 Depth3 { get; set; }
}
public class TestClassDepth3 {
public TestClassDepth4 Depth4 { get; set; }
}
public class TestClassDepth4 {
public string Name { get; set; }
}
}
Controller (HomeController.cs):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MvcApplication3.Models;
namespace MvcApplication3.Controllers
{
[HandleError]
public class HomeController : Controller
{
public ActionResult Index()
{
ViewData["Message"] = "Welcome to ASP.NET MVC!";
return View();
}
public ActionResult About()
{
ViewResult vr = View(
new MyData()
{
Customers =
new List<MyCustomer>()
{
new MyCustomer()
{ Depth1=new TestClassDepth1()
{ Depth2=new TestClassDepth2(){
Depth3=new TestClassDepth3(){
Depth4=new TestClassDepth4(){
Name="Allen Chen"}
}
}
}
},
new MyCustomer()
{ Depth1=new TestClassDepth1()
{ Depth2=new TestClassDepth2(){
Depth3=new TestClassDepth3(){
Depth4=new TestClassDepth4(){
Name="Peter wang"}
}
}
}
},
new MyCustomer()
{ Depth1=new TestClassDepth1()
{ Depth2=new TestClassDepth2(){
Depth3=new TestClassDepth3(){
Depth4=new TestClassDepth4(){
Name="Mike Sun"}
}
}
}
}
}
});
return vr;
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Test(FormCollection formValues)
{
MyData md = new MyData();
UpdateModel(md, "MyData", formValues.ToValueProvider());
//Set a breakpoint here to check
//md.Customers[1].Depth1.Depth2.Depth3.Depth4.Name
//md.Customers[2].Depth1.Depth2.Depth3.Depth4.Name
//md.Customers[3].Depth1.Depth2.Depth3.Depth4.Name
ViewResult vr = View("About", md);
return vr;
}
}
}
About.aspx:
<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage" %>
<asp:Content ID="aboutTitle" ContentPlaceHolderID="TitleContent"
runat="server">
About Us
</asp:Content>
<asp:Content ID="aboutContent" ContentPlaceHolderID="MainContent"
runat="server">
<h2>About</h2>
<form action="Test" method="post" >
<%for (int i = 0; i <
((MvcApplication3.Models.MyData)ViewData.Model).Customers.Count; i++)
{ %>
<%=Html.TextBox("MyData.Customers[" + i +
"].Depth1.Depth2.Depth3.Depth4.Name",
((MvcApplication3.Models.MyData)ViewData.Model).Customers[i].Depth1.Depth2.D
epth3.Depth4.Name)%>
<%} %>
<input type="submit" value="Post" />
</form>
</asp:Content>
To test, set a breakpoint in the Test method where the comments locate,
start debugging, click the "About" button to see the view. You'll see data
is filled in the textbox. You can change the input if you like. Then click
"Post" button to break into the Test method. Check the values indicated by
the comments in the watch window. You'll see they are all updated.
Please have a try and let me know if it works. If you have additional
questions please feel free to ask.
Regards,
Allen Chen
Microsoft Online Support
Delighting our customers is our #1 priority. We welcome your comments and
suggestions about how we can improve the support we provide to you. Please
feel free to let my manager know what you think of the level of service
provided. You can send feedback directly to my manager at:
msd...@microsoft.com.
==================================================
Get notification to my posts through email? Please refer to
http://msdn.microsoft.com/en-us/subscriptions/aa948868.aspx#notifications.
Note: MSDN Managed Newsgroup support offering is for non-urgent issues
where an initial response from the community or a Microsoft Support
Engineer within 2 business day is acceptable. Please note that each follow
up response may take approximately 2 business days as the support
professional working with you may need further investigation to reach the
most efficient resolution. The offering is not appropriate for situations
that require urgent, real-time or phone-based interactions. Issues of this
nature are best handled working with a dedicated Microsoft Support Engineer
by contacting Microsoft Customer Support Services (CSS) at
http://msdn.microsoft.com/en-us/subscriptions/aa948874.aspx
==================================================
This posting is provided "AS IS" with no warranties, and confers no rights.
Yes, your code works fine, which I guess proves that deep models can be
built by the model binder.
I did make an error in my original post. I had been playing around with
different references, including using the local reference which was posted.
If I use the following, then the model still does not get rebuilt, although
the ContactDatas object is built, just it's contents are null:
Html.TextArea("venue.VenueDetail.ContactLink.ContactDatas["+Model.Venue.VenueDetail.ContactLink.ContactDatas.IndexOf(contact)+"].Data")
Any ideas why this might not be working?
What is the best way to simplify the model within the model itself, or
within the controller by providing a formviewmodel (as per the nerdinners
application). I am stuck with the LinktoSQL produced classes, The only real
dead wood is the ContactLink Class, which is just a relic of the database
structure. I could combine Venue and VenueDetails in one big class, though
feel that actually they are better as seperate classes.
Thanks,
Richard
"Allen Chen [MSFT]" <all...@online.microsoft.com> wrote in message
news:Dd2ApeE2...@TK2MSFTNGHUB02.phx.gbl...
I've actually found an answer which works for this on a StackOverflow
posting. I'm still not quite sure why my code didn't work, other than that
the model was too complex for the DefaultModelBinder.
The solution is to add a second Parameter with a Bind attribute Prefix as
follows:
[Bind(Prefix = "venue.VenueDetail.ContactLink.ContactDatas")]
EntitySet<ContactData> contactData
Then obviously set the ContactDatas = contactData.
I need to look at exactly how Prefix is used, but it does work in populating
the ContactDatas object.
With respect to the complexity of my model I would appreciate some advice on
whether it is advisable to be simplifying the model produced by the
LinqtoSQL and if so the best approach to doing this.
Thanks,
Richard
>I've actually found an answer which works for this on a StackOverflow
>posting. I'm still not quite sure why my code didn't work, other than that
>the model was too complex for the DefaultModelBinder.
Thanks for your update. I tried again and it still works. Below is the
updated code, which I believe is more close to your scenario:
Model:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace MvcApplication3.Models
{
public class MyData
{
public MyDataDepth1 MyDataDepth1 { get; set; }
}
public class MyDataDepth1
{
public MyDataDepth2 MyDataDepth2 { get; set; }
}
public class MyDataDepth2
{
public List<MyCustomer> Customers { get; set; }
}
public class MyCustomer {
public TestClassDepth1 Depth1 { get; set; }
}
public class TestClassDepth1 {
public TestClassDepth2 Depth2{get;set;}
}
public class TestClassDepth2
{
public TestClassDepth3 Depth3 { get; set; }
}
public class TestClassDepth3 {
public TestClassDepth4 Depth4 { get; set; }
}
public class TestClassDepth4 {
public string Name { get; set; }
}
}
Controller:
public ActionResult About()
{
ViewResult vr = View(
new MyData()
{
MyDataDepth1 = new MyDataDepth1()
{
MyDataDepth2 = new MyDataDepth2()
}
}
}
});
UpdateModel(md,"MyData", formValues.ToValueProvider());
ViewResult vr = View("About", md);
return vr;
}
View:
<%for (int i = 0; i <
((MvcApplication3.Models.MyData)ViewData.Model).MyDataDepth1.MyDataDepth2.Cu
stomers.Count; i++)
{ %>
<%=Html.TextBox("MyData.MyDataDepth1.MyDataDepth2.Customers[" + i +
"].Depth1.Depth2.Depth3.Depth4.Name",
((MvcApplication3.Models.MyData)ViewData.Model).MyDataDepth1.MyDataDepth2.Cu
stomers[i].Depth1.Depth2.Depth3.Depth4.Name)%>
<%} %>
To know the reason why your code cannot work could you tell me what are
venue, VenueDetail, ContactLink and ContactDatas (especially venue, it's in
lower case, is it still a variable?) ? You can send a demo project to me:
all...@microsoft.com. Please update here after sending the project in case
I missed that email.
I have tried your code and once again it works appropriately. I have sent
you an email with a stripped down version of my project which fails in the
same way on trying to create a venue.
I notice that your Post method processes a FormCollection. Mine processes a
Venue venue as per the Nerddinner example. I have tried following your
FormCollection approach in my project and received the same result.
I am happy that using [Bind (Prefix=...)] does solve the issue, and I sent
the example to you just out of my interest and hopefully yours.
If establishing the reason for the failure requires extensive work, then
please feel free to let me know and discard.
Richard
If you use ViewPage<T>, you have a more explicit way of defining your model
(ie, T) so you can drill through more explicitly.
--
Gregory A. Beamer
MVP; MCP: +I, SE, SD, DBA
Blog: http://gregorybeamer.spaces.live.com
Twitter: @gbworld
*************************************************
| Think outside the box! |
*************************************************
"RichB" <richsa...@community.nospam> wrote in message
news:#iCVv1#1JHA...@TK2MSFTNGP06.phx.gbl...
Allen, does your example from a few days ago work when you change the
MyData.Customers type from List<MyCustomer> to
EntitySet<MyCustomer> ??
I keep running into LOTS of posts around the 'nets describing problems
with this same type of situation, but so far, not too many solutions
(unless you consider writing your own model binder to be a viable
solution!)
Thanks!
Since it seems that when I change my code from EntitySet<Contact> to
List<Contact>, the MVC default model binder starts working as expected
(even though the LTS isn't now), I figured I would provide an
alternate, "aliased" property to MVC that is of type List<Contact>,
and sure enough, this seems to work.
In my Company entity class:
// This is what LINQ-to-SQL will use:
private EntitySet<Contact> _Contacts = new EntitySet<Contact>();
[Association(Storage="_Contacts", OtherKey="CompanyID", ThisKey="ID")]
public EntitySet<Contact> Contacts
{
get { return _Contacts; }
set { _Contacts.Assign(value); }
}
// This is what MVC default model binder (and my View) will use:
public List<Contact> MvcContacts
{
get { return _Contacts.ToList<Contact>(); }
set { _Contacts.AddRange(value); }
}
So now, in my View, I have the following:
<label>First Name*
<%= Html.TextBox("Company.MvcContacts[" + i + "].FirstName") %>
</label>
<label>Last Name*
<%= Html.TextBox("Company.MvcContacts[" + i + "].LastName") %>
</label>
Seems to work like a charm!
Best of luck!
-Mike
>I have tried your code and once again it works appropriately. I have sent
>you an email with a stripped down version of my project which fails in the
>same way on trying to create a venue.
>I notice that your Post method processes a FormCollection. Mine processes
a
>Venue venue as per the Nerddinner example. I have tried following your
>FormCollection approach in my project and received the same result.
>I am happy that using [Bind (Prefix=...)] does solve the issue, and I sent
>the example to you just out of my interest and hopefully yours.
I've reproduced this issue. The cause of this problem is, in the set of
ContactDatas property, the auto generated code by Linq to SQL is:
_ContactDatas.Assign(value);
However, in the model updating stage the above set accessor will be
invoked. The value passed to it is exactly the same object as the
_ContactDatas. Therefore it will cause _ContactDatas clear itself, as the
same result of this test:
EntitySet<ContactData> test = new EntitySet<ContactData>();
test.Add(new ContactData());
test.Assign(test);
//test is empty now
The reason of above behavior is, in the EntitySet<T>.Assign method, it
clears all the items of the caller of this method (the test variable in the
above code) and then loop through the collection that is tend to be
assigned to the caller collection. Unfortunately it's been cleared out so
there's no item to add.
You can try this workaround if you like:
Controller:
public ActionResult Create(FormCollection formValues
//Include Bind and success
//, [Bind(Prefix="venue.VenueDetail.ContactLink.ContactDatas")]
EntitySet<ContactData> cont
)
{
Venue venue = new Venue();
UpdateModel(venue, "Venue", formValues.ToValueProvider());
//check venue.VenueDetail.ContactLink.ContactDatas[0].Data));
return RedirectToAction("Create");
}
View:
<%=Html.TextBox("Venue.VenueDetail.ContactLink.ContactDatas[0].Data")%>
Modle:
[Association(Name = "ContactLink_ContactData", Storage =
"_ContactDatas", ThisKey = "ContactLinkId", OtherKey = "ContactLinkId")]
public EntitySet<ContactData> ContactDatas
{
get
{
return this._ContactDatas;
}
set
{
/////////////
//to work around, add this:
_ContactDatas = new EntitySet<ContactData>();
_ContactDatas.Assign(value);
}
}
I personally don't think this behavior of Assign method is good. It'd be
better to leave the caller collection not changed if the parameter of the
method is the same object as the caller. You're suggested to submit a
feedback in our connect site to inform our develop team of this issue.
They'll investigate it and hope they could provide revised version in the
next release.
https://connect.microsoft.com/VisualStudio/Feedback?wa=wsignin1.0
>I notice that your Post method processes a FormCollection. Mine processes
a
>Venue venue as per the Nerddinner example. I have tried following your
>FormCollection approach in my project and received the same result.
>I am happy that using [Bind (Prefix=...)] does solve the issue, and I sent
>the example to you just out of my interest and hopefully yours.
>If establishing the reason for the failure requires extensive work, then
>please feel free to let me know and discard.
>Have you tried my workaround? Can it work?
Sorry I've been tied up all of last week and not had chance to try our work
around. I shall probably get to do it tomorrow sometime and will post back
then.
Richard
"Allen Chen [MSFT]" <all...@online.microsoft.com> wrote in message
news:KT9k2Jd3...@TK2MSFTNGHUB02.phx.gbl...
I can't say that I fully understand why the [Bind (Prefix=..)] solution
works where default binding fails, but For my purposes I am just happy that
I have solutions which I can apply.
I have provided feedback on the link you provided, hopefully in the correct
and understandable way.
Many thanks, for your help.
Richard
"Allen Chen [MSFT]" <all...@online.microsoft.com> wrote in message
news:sjSYxAr2...@TK2MSFTNGHUB02.phx.gbl...