MVC - Model binding to collection


R

RichB

I am slowly getting to grips with the default model binding within the MVC
framework, but within the model I am looking at now the model will not bind
for a collection.

I have read Scott Hanselman's blog post:
http://www.hanselman.com/blog/ASPNE...dingToArraysListsCollectionsDictionaries.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
 
Ad

Advertisements

C

Cowboy \(Gregory A. Beamer\)

I got a headache looking at what you posted, but if I understand the gist of
it, you are wondering how to get an individual item from a list? If so, you
have to simplify. You should not have to pass a godawfully long string of
HTTP encoded crap to get info on a single item. If you want to drill down n
number of steps in a hierarchy, create a page to display that item so you
can get it with a simple:

http://mysite/item3/1

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 said:
I am slowly getting to grips with the default model binding within the MVC
framework, but within the model I am looking at now the model will not
bind
for a collection.

I have read Scott Hanselman's blog post:
http://www.hanselman.com/blog/ASPNE...dingToArraysListsCollectionsDictionaries.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
 
R

RichB

Sorry,
perhaps I should be clearer, I am wanting to create an object which has a
collection. So a Venue has some details part of which are a series of
contact details (phone, email etc). I am creating the view without problem,
but my expectation is that I can create a textbox with a name which will
enable the defaultmodelbinder to map back the post variables to the model.

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) said:
I got a headache looking at what you posted, but if I understand the gist
of it, you are wondering how to get an individual item from a list? If so,
you have to simplify. You should not have to pass a godawfully long string
of HTTP encoded crap to get info on a single item. If you want to drill
down n number of steps in a hierarchy, create a page to display that item
so you can get it with a simple:

http://mysite/item3/1

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 said:
I am slowly getting to grips with the default model binding within the
MVC
framework, but within the model I am looking at now the model will not
bind
for a collection.

I have read Scott Hanselman's blog post:
http://www.hanselman.com/blog/ASPNE...dingToArraysListsCollectionsDictionaries.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
 
A

Allen Chen [MSFT]

Hi Richard,
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.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:
(e-mail address removed).

==================================================
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.
 
R

RichB

Thanks Allen,

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 said:
Hi Richard,
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.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:
(e-mail address removed).

==================================================
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.
 
R

RichB

Allen,

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
 
Ad

Advertisements

A

Allen Chen [MSFT]

Hi 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()
{
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());

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.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:
(e-mail address removed). Please update here after sending the project in case
I missed that email.


Regards,
Allen Chen
Microsoft Online Support
 
R

RichB

Thanks Allen,

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
 
C

Cowboy \(Gregory A. Beamer\)

Are you using ViewPage or ViewPage<T>?

If you use ViewPage<T>, you have a more explicit way of defining your model
(ie, T) so you can drill through more explicitly.




RichB said:
Sorry,
perhaps I should be clearer, I am wanting to create an object which has a
collection. So a Venue has some details part of which are a series of
contact details (phone, email etc). I am creating the view without
problem, but my expectation is that I can create a textbox with a name
which will enable the defaultmodelbinder to map back the post variables to
the model.

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) said:
I got a headache looking at what you posted, but if I understand the gist
of it, you are wondering how to get an individual item from a list? If so,
you have to simplify. You should not have to pass a godawfully long string
of HTTP encoded crap to get info on a single item. If you want to drill
down n number of steps in a hierarchy, create a page to display that item
so you can get it with a simple:

http://mysite/item3/1

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 said:
I am slowly getting to grips with the default model binding within the
MVC
framework, but within the model I am looking at now the model will not
bind
for a collection.

I have read Scott Hanselman's blog post:
http://www.hanselman.com/blog/ASPNE...dingToArraysListsCollectionsDictionaries.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
 
F

funka420

I am having the same issue, Richard. I believe it has to do with a
collection being an EntitySet<T> rather than a List<T>.

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!
 
F

funka420

Well, shortly after I posted my reply, I seem to have found a
workaround that is working in my situation. In my case, I have a
Company entity that has one to many relationship to Contacts (my Linq-
to-Sql EntitySet).

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
 
Ad

Advertisements

A

Allen Chen [MSFT]

Hi Richard,
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

Regards,
Allen Chen
Microsoft Online Support
 
A

Allen Chen [MSFT]

Hi Richard,
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?

Regards,
Allen Chen
Microsoft Online Support
 
R

RichB

Hi Allen,

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 said:
Hi Richard,
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?

Regards,
Allen Chen
Microsoft Online Support
 
Ad

Advertisements

R

RichB

Thanks Allen, yes your workaround did work as expected, as does the [Bind
(Prefex = ..)] solution.

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 said:
Hi Richard,
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

Regards,
Allen Chen
Microsoft Online Support
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top