ModelBinder for ASP.NET MVC RC1

Posts   
 
    
Posts: 134
Joined: 10-Jan-2007
# Posted on: 29-Jan-2009 23:38:54   

This is a ModelBinder to use with the new ASP.NET MVC framework RC1. This will automatically create and populdate LLBLGen objects from form post, query string and route data. Works with both SelfServicing and Adapter.

An overview of Model Binding in ASP.NET MVC can be found here: http://weblogs.asp.net/scottgu/archive/2008/09/02/asp-net-mvc-preview-5-and-form-posting-scenarios.aspx and here: http://weblogs.asp.net/scottgu/archive/2008/10/16/asp-net-mvc-beta-released.aspx#three

This implementation inherits from the provided DefaultModelBinder and defers most of the work to it. For non LLBLGen entities, they just pass through to the default implementation. This code is to help set the primary keys and stop infinite recursive Parent-Child-Parent calls.

Requirements

  • LLBLGen Pro v2.6
  • .NET 3.5 SP1
  • ASP.NET MVC RC1

Usage

Add the LLBLGenModelBinder.cs to your project

In the Global.asax.cs, add this to the Application_Start(): ModelBinders.Binders.DefaultBinder = new LLBLGenModelBinder();

You will be able to write code like this and have the entities automatically populated from the request

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Edit(CompanyEntity company)
    {
        if (ModelState.IsValid)
        {
            company.Save(true);
            return RedirectToAction("Edit", new { id = company.CompanyId });
        }
        else
        {
            return View(company);
        }
    }

How it works

In order for the Model Binding to find the correct fields, they need to be named correctly.

For the top level object, the take the form parameterName.Field so: <%= Html.TextBox("company.CompanyId", ViewData.Model.CompanyId)%>

For 1-to-1 objects, they look like <%= Html.TextBox("company.NormalAddress.Address", ViewData.Model.NormalAddress.Address)%>

For 1-to-many it gets a little more complex: The RC1 release behaviour changed from the beta. The beta looked for a .index property and looped through them to fill the collection RC1 ONLY processes for sequential indexes starting with 0.

I have implemented the beta behaviour if an index field exists, otherwise it passes through to the RC1 method. If you do not want this behaviour at all, remove the region "handle Beta style collections with .index"

RC1 behaviour: Name your fields company.Contacts[0].FieldName, counting up from 0

<% for (int i=0;i<Model.Contacts.Count;i++) 
    ContactEntity contact = Model.Contacts[i]; {%>
<tr>
    <td>Contacts.ContactId</td>
    <td><%= Html.TextBox("company.Contacts[" + i + "].ContactId", contact.ContactId)%></td>
</tr>
<tr>
    <td>Contacts.FirstName</td>
    <td><%= Html.TextBox("company.Contacts[" + i + "].FirstName", contact.FirstName)%></td>
</tr>
<tr>
    <td>Contacts.LastName</td>
    <td><%= Html.TextBox("company.Contacts[" + i + "].LastName", contact.LastName)%></td>
</tr>
<% } %>

To add, you need to add a new set of fields numbered one higher.

Beta behaviour: looks for a field (.index) that holds the index values to iterate:

<% foreach (ContactEntity contact in Model.Contacts) {%>
<tr>
    <td>Contacts.index</td>
    <td><%= Html.TextBox("company.Contacts.index", contact.ContactId)%></td>
</tr>
<tr>
    <td>Contacts.ContactId</td>
    <td><%= Html.TextBox("company.Contacts[" + contact.ContactId + "].ContactId", contact.ContactId)%></td>
</tr>
<tr>
    <td>Contacts.FirstName</td>
    <td><%= Html.TextBox("company.Contacts[" + contact.ContactId + "].FirstName", contact.FirstName)%></td>
</tr>
<tr>
    <td>Contacts.LastName</td>
    <td><%= Html.TextBox("company.Contacts[" + contact.ContactId + "].LastName", contact.LastName)%></td>
</tr>
<% } %>

if you want to add a new object, create any unique index: <%= Html.TextBox("company.Contacts.index", -1)%> <%= Html.TextBox("company.Contacts[-1].ContactId", 0)%> <%= Html.TextBox("company.Contacts[-1].FirstName")%>

NOTES

The Model Binder sets IsNew if a primary key field is set to a meaningful value (for ints > 0, strings not blank, etc.). You can change this behaviour by providing your own form field (i.e. company.IsNew) AND removing the "IsNew" from the list of excluded fields in the LLBLGenModelBinder

The binder only follows relations where the entity is on the PK Side, this keeps circular references from spinning out.

This process "creates" child objects, it does not "syncronize" child objects. It can syncronize objects, but you need to use the UpdateModel method to manually walk the child elements of 1-to-many. In this case, DO NOT output the .index form element because they will be in the child collection twice.

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Edit2(int id)
    {
        CompanyEntity company = new CompanyEntity(id);
        UpdateModel(company, "company");
        foreach (ContactEntity contact in company.Contacts)
        {
            UpdateModel(contact, "company.Contacts[" + contact.ContactId + "]");
        }

        if (ModelState.IsValid)
        {
            company.Save(true);
            return RedirectToAction("Edit", new { id = company.CompanyId });
        }
        else
        {
            return View(company);
        }
    }
Attachments
Filename File size Added on Approval
LLBLGenModelBinder.cs 15,261 29-Jan-2009 23:39.12 Approved
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39613
Joined: 17-Aug-2003
# Posted on: 30-Jan-2009 10:10:16   

Thanks for the contribution, Brian!! smile

Frans Bouma | Lead developer LLBLGen Pro
Posts: 134
Joined: 10-Jan-2007
# Posted on: 03-Feb-2009 20:37:08   

I have updated the implementation to handle beta style collections using a .index field for collections/lists, arrays and dictionary. There are now 2 files, the first is IndexModelBinder that handles the index, the other is LLBLGenModelBinder that handles entities. It does not look like the .index will make it to the RTM (see http://forums.asp.net/p/1377775/2913408.aspx )

Notes on usage are basically the same, see the files.

Brian

Attachments
Filename File size Added on Approval
LLBLGenModelBinder.zip 6,034 03-Feb-2009 20:38.37 Approved
NickD
User
Posts: 224
Joined: 31-Jan-2005
# Posted on: 05-Mar-2009 22:55:23   

Brian,

~~I'm so close on this, but can't quite get it to work for me on the POST ActionResult. I can't get UpdateModel() to return my EntityCollection from the model. Any chance you want to look at my code and see where I may be going wrong?

I should say that your code worked wonderfully, I'm just stuck and I know it's something small I've done wrong on my side.~~

I've found out how to make it work. Previously, I had my view typed to an EntityCollection<CallLogEntity> and I could never get it to work. I noticed in your examples that you had used a single entity and then looped through the Contacts collection for the Company entity. I changed my view to be typed to a single entity and then looped through each CallLog in my CallLogCollection attached to that single entity. That did the trick.

So now my question is, how to make this work if the view is typed to an EntityCollection. It seems like it should work, so where was I going wrong?

nasser
User
Posts: 12
Joined: 23-Jun-2004
# Posted on: 12-Apr-2009 14:16:38   

Brain,

What do you think about calling Authorizer objects from the LLBLGenModelbinder?

I am considering something like this:


[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int Id)
{
      ContactEntity contactToEdit = _service.GetContact(Id); 

      UpdateModel(contactToEdit);

      // on goes the execution

In SetProperty in LLBLGenModelBinder I added the following test


              if (entity.AuthorizerToUse != null && !entity.AuthorizerToUse.CanSetFieldValue(entity, entity.GetFieldByName(propertyDescriptor.Name).FieldIndex))
              {
                return;
              }


So properties that should not be changeable will not be changed by values coming in from the client. I would appreciate any comments on this!

Nasser

ps Happy Easter to all those celebrating!

tprohas
User
Posts: 257
Joined: 23-Mar-2004
# Posted on: 17-Jun-2009 21:10:24   

Hello all,

I have a question about how the model binder works with regards to the IsValid property. I have just implemented this model binder and am using it along with the ExtJs javascript library and so far it seems to be working. All of the property values are being passed to my entity from the FormPanel. The only thing that is not happening is that the ModelState.IsValid property is always false.

Can anyone tell me why this might be happening?

nasser
User
Posts: 12
Joined: 23-Jun-2004
# Posted on: 19-Jun-2009 21:47:11   

Could you please check the error you have in the ModelState? Should the error be caused by an element called "Id" (it usually is the first element in list in ModelState), you can solve the problem by telling MVC not to try and bind this property:

public ActionResult Create([Bind(Exclude = "Id")]ContactEntity contactToCreate)

[Bind(Exclude = "Id")] does that.

You can read about the problem by searching for

asp.net mvc modelstate isvalid "A value is required" ModelState["Id"]

Should you be actually excpecting an element called Id, then I am afraid that I a have no ready answer for you.

Posts: 134
Joined: 10-Jan-2007
# Posted on: 10-Nov-2009 15:50:59   

After some usage, I have made a tweak to how the binder selects the properties to bind. Before, the binder was only following properties where the entity was on the PK side (so, down the tree).

In theory, this should stop circular references.

In practicality, the way the binder builds names using the parent path and how it checks for the existence of any sub fields from the client before creating the property keeps this from happening.

Attached are the changes.

Nasser...

What do you think about calling Authorizer objects from the LLBLGenModelbinder?

Sorry I did not see your comment on the authorizer. The way the binder sets properties is through reflection using propertyDescriptor.SetValue. This means that all the normal code for setting a property works. So, OnCanSetFieldValue is getting called which checks the authorizer.

The only field this is not true for is the PK field since it does a ForcedCurrentValueWrite, so you could add code to check that one.

Brian

Attachments
Filename File size Added on Approval
LLBLGenModelBinder.zip 6,042 10-Nov-2009 15:51.16 Approved
Posts: 134
Joined: 10-Jan-2007
# Posted on: 19-Nov-2009 18:13:23   

Some notes for ASP.NET MVC 2 Beta:

The team added back support for the .index on the collections. You should be able to just change the LLBLGenModelBinder to inherit from the IndexModelBinder and move the static ShouldUpdateProp method from the IndexModelBinder to the LLBLGenModelBinder. I have attached the file with the changes.

When the RC comes out, I will revisit and actually test.

Brian

Attachments
Filename File size Added on Approval
LLBLGenModelBinder.cs 12,004 20-Nov-2009 15:18.48 Approved
NickD
User
Posts: 224
Joined: 31-Jan-2005
# Posted on: 19-Nov-2009 19:02:52   

Brian - Thanks for all your work on this. I use this in two apps that are currently used in my intranet and it just works.

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 20-Nov-2009 04:26:32   

Brian, I tested the old one and I like this update for MVC 2 Beta as well. This saves us some roundtrips to db in the POST. Thank for the contribution wink

David Elizondo | LLBLGen Support Team
arschr
User
Posts: 893
Joined: 14-Dec-2003
# Posted on: 20-Nov-2009 15:16:23   

Brian, is the attachment you reference in your 11/19/2009 message missing or is it the attachment from your 11/10/2009 message?

Posts: 134
Joined: 10-Jan-2007
# Posted on: 20-Nov-2009 15:20:00   

I thought I attached it.

Just attached it to the 11/19 message (againsmile )

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39613
Joined: 17-Aug-2003
# Posted on: 20-Nov-2009 19:09:54   

It wasnt approved yet, now it is simple_smile Thanks a million, Brian!

Frans Bouma | Lead developer LLBLGen Pro
TomDog
User
Posts: 618
Joined: 25-Oct-2005
# Posted on: 24-Dec-2009 00:44:07   

Thanks heaps for your model binder Briansimple_smile We've been using for nearly a year now but one thing that recently became apparent is that it doesn't support inheritance. i.e. If the view is typed to a base type and a descendent type is sent up to the view the base type comes back to the action rather than the descendent type. And, from memory, it came back with the discriminator value of the descendent type. To enable us to get back the same type we sent up we’ve had to subclass LLBLGenModelBinder and create our own model deserializer which created the right descendent type. However this deserializer is specific to our entities, I tried but couldn’t find any way of doing a generic LLBL deserializer(everything I need to do that seemed locked down). Does anyone know any better?

Jeremy Thomas
daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 24-Dec-2009 06:55:49   

TomDog wrote:

Thanks heaps for your model binder Briansimple_smile We've been using for nearly a year now but one thing that recently became apparent is that it doesn't support inheritance. i.e. If the view is typed to a base type and a descendent type is sent up to the view the base type comes back to the action rather than the descendent type. And, from memory, it came back with the discriminator value of the descendent type. To enable us to get back the same type we sent up we’ve had to subclass LLBLGenModelBinder and create our own model deserializer which created the right descendent type. However this deserializer is specific to our entities, I tried but couldn’t find any way of doing a generic LLBL deserializer(everything I need to do that seemed locked down). Does anyone know any better?

You could share your specific subclass code here to see if someone can make it usable in more general cases.

David Elizondo | LLBLGen Support Team
Posts: 134
Joined: 10-Jan-2007
# Posted on: 28-Dec-2009 16:19:49   

TomDog,

Can you post the specific class/lpg file, usage in your controller or ViewModel and your descendent model binder where you are setting the type?

Brian

Zink
User
Posts: 2
Joined: 29-Dec-2009
# Posted on: 29-Dec-2009 19:06:29   

@brianchance Thanks a bunch of putting this modelbinder together - I've been able to populate a pretty complicated object graph, saving tons of time and lines of code!

@TomDog I ran into this same issue where the modelbinder was creating basetype entities instead of their derived types. I managed to solve the issue by adding some code to the modelbinder and then inserting a hidden field into my HTML that contains the entity's derived class details:

protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) {
  // check to see if we need to cast the model to a specific type...
  if (bindingContext.ValueProvider.Keys.Contains(bindingContext.ModelName + ".CastAs"))
    modelType = Type.GetType(bindingContext.ValueProvider[bindingContext.ModelName + ".CastAs"].AttemptedValue);

  return base.CreateModel(controllerContext, bindingContext, modelType);
}

And then in the HTML (assuming you pass your view a model of the derived type):

<%= Html.Hidden("EntityClass.CastAs", Model.GetType().AssemblyQualifiedName) %>

There's probably a better way of making the modelbinder convert the entity to its derived type, but I am unawares as to how to best do that wink

Posts: 134
Joined: 10-Jan-2007
# Posted on: 29-Dec-2009 20:32:48   

@Zink - thanks for the code post, I think I understand the issue now.

As I understand it, you want the model binder to bind a parameter declared as a ParentEntity, but actually create a ChildEntity. Correct?

This is actually an issue with the ModelBinder in general. I see 2 a few work arounds.

  1. Exactly what you did.
  2. Create an instance of the ChildEntity and use UpdateModel in the controller instead of the automatic parameter binding.
ChildEntity entity = new ChildEntity();
this.UpdateModel(entity);

I added a version of #1 to the LLBLGenModelBinder class, though instead of "CastAs", I used "OverrideModelType". So it would be:

<%= Html.Hidden("EntityClass.OverrideModelType", Model.GetType().AssemblyQualifiedName) %>

NOTE: you can also use typeof to get the name:

typeof(EntityClass).AssemblyQualifiedName 

Thanks all for the input.

Brian

Zink
User
Posts: 2
Joined: 29-Dec-2009
# Posted on: 29-Dec-2009 21:08:03   

@Brian - yup, sounds like you understand the problem.

With my approach, while I don't really like having the derived entity's assembly details in a hidden field in my markup, I think it is a small price to pay for having all the modelbinding just automatically work!

TomDog
User
Posts: 618
Joined: 25-Oct-2005
# Posted on: 01-Jan-2010 12:39:43   

Zink wrote:

With my approach, while I don't really like having the derived entity's assembly details in a hidden field in my markup

I don't like that either though I must admit I didn't even think of doing that. What I've done is to obtain the correct derived type based on the discriminator value. I was actually creating the model myself rather than just obtaining the type but here is the code to find the correct factory:

var factory = ModelHelper.GetFactory(modelType, GetDiscriminatorValue(modelType, serializedModel));

which calls:


/// <summary>
/// Gets the the correct subtype factory given the discriminator value.
/// </summary>
/// <param name="superTypeOfEntity">The super type of entity.</param>
/// <param name="discriminatorValue">The discriminator value.</param>
/// <returns>factory to use or null if not found</returns>
public static IEntityFactory2 GetFactory(Type superTypeOfEntity, object discriminatorValue)
{
    var factory = EntityFactoryFactory.GetFactory(superTypeOfEntity);
    if (discriminatorValue != null)
        if (typeof(WorkRequestEntity).IsAssignableFrom(superTypeOfEntity))
        {
            factory = factory.GetEntityFactory(new[] { null, discriminatorValue }, null);
        }
    return factory;
    }

private static object GetDiscriminatorValue(Type modelType, IDictionary<string, object> serializedModel)
{
    if (typeof (WorkRequestEntity).IsAssignableFrom(modelType))
    {
        if (serializedModel.ContainsKey("WRType"))
            return serializedModel["WRType"];
    }
    return null;
}

WorkRequestEntity is the supertype and WRType is the discriminator field.

An example action is

public ActionResult SaveRiskReview(RiskReviewEntity wr);

and page is bound and contains the discriminator value:

Inherits="System.Web.Mvc.ViewPage<RiskReviewEntity>"

WorkRequestEntity is 'abstract' but the page edits the derived type RiskReviewEntity or one of 4 RiskReviewEntity subtypes. This all works fine apart from that I have to hardcode stuff specific to my model. What I couldn't figure out is a general way of getting the discriminator field name for an entity.

Jeremy Thomas
Posts: 134
Joined: 10-Jan-2007
# Posted on: 04-Feb-2010 16:13:20   

I have updated the ModelBinder for MVC 2 RC. The new thread can be found here:

http://www.llblgen.com/tinyforum/Messages.aspx?ThreadID=17343

Brian

Posts: 134
Joined: 10-Jan-2007
# Posted on: 04-Feb-2010 22:29:55   

The update to MVC 2 RC brought to light a binding bug when setting a nullable field (int?) that previously had a value to null. The field would not be marked changed and so the null would not get saved to the database.

The attached adds a fix that sets fields set to null to be changed.

Brian

Attachments
Filename File size Added on Approval
LLBLGenModelBinder.zip 6,372 04-Feb-2010 22:30.04 Approved