Sponsored By Aspose - File Format APIs for .NET

Aspose are the market leader of .NET APIs for file business formats – natively work with DOCX, XLSX, PPT, PDF, MSG, MPP, images formats and many more!

Passing objects to controller actions in ASP.NET MVC

Here’s the bad thing about ASP.NET MVC. Every little thing about it is bloggable mostly because every little thing is new to it. I’m half considering using Monorail just because everything in it is so well-documented, I wouldn’t need to waste my time blogging about it. (Those of you about to regurgitate the holy war about why ASP.NET MVC has caused all this hoopla when Monorail has been around for so long, calm down. It’s just software, for Jayzus’ sake.)

After lamenting the mechanisms I needed to use to pass information to a ControllerAction, Ben Scheirman turned me on to the ConventionController in MvcContrib. Apparently, it works well for pages that create new objects but not so much for ones that update existing objects.

*EDIT*

I originally had a whole long spiel done that included a history of my stored procedure-writing prowess and a PurportedSibling domain object. It was to explain that because of my genius way of setting up my service, I could use the ConventionController for both creates and updates.

But the fact is, it had nothing to do with how I set up my service. It just works out of the box. By deriving my controller from ConventionController, I can create a view that includes:

    using ( Html.Form( "Save", "Job", FormExtensions.FormMethod.post ) )
    {%>
    <%=Html.Hidden( "job.Id", ViewData.Id ) %>
    Sibling Name: <%=Html.TextBox( "sibling.Name", ViewData.Name ) %>
    Is half sibling?: <%=Html.RadioButtonList( "sibling.IsHalfBrother", TRISTATE_ENUM, ViewData.IsHalfBrother ) %>
    Rumoured mammy: <%=Html.TextBox( "sibling.RumouredMammy", ViewData.RumouredMammy ) %>
    Rumoured pappy: <%=Html.TextBox( "sibling.RumouredPappy", ViewData.RumouredPappy ) %>
    <%=Html.SubmitButton( "Submit", "Save" ) %>
    <%}%>

Here are the actions. The first launches this view for a new sibling. The second for updating an existing one. And the third saves in both cases.

    public void Create( )
    {
        RenderView( Edit", PurportedSibling.Null( ) );
    }

    public void Edit( int id )
    {
        PurportedSibling sibling = _siblingService.GetById( id );
        RenderView( "Edit", sibling );
    }

    public void Save( [Deserialize("sibling")] PurportedSibing sibling )
    {
        _siblingService.Save( sibling );
        RedirectToAction( new { Action = "SiblingUpdated", id = sibling.Id } );
    }

Crisp and clean be how I like my controller actions. Compare the Save with what it looked like before:

[ControllerAction]
public void Save( int siblingId, string siblingName, TRISTATE isHalfSibling, string rumouredMammy, string rumouredPappy )
{
	_siblingService.Save( siblingId, siblingName, isHalfSibling, rumouredMammy, rumouredPappy );
        RedirectToAction( new { Action = "SiblingUpdated", id = sibling.Id } );
}

This was before I was about to add the part where you specify possible offspring of the siblings on the same screen, something that would have made this Save method more complicated than the domain itself, which is an homage to graph theory in and of itself.

In any case, I’ll leave the implementation of the Save method to your imagination in both cases. (Hint: It is much nicer in the new version, let me tell you!)

Notice how the new version also doesn’t have a [ControllerAction] attribute. That’s another feature of the ConventionController which will make it much easier to manage when the next CTP of ASP.NET MVC comes out which removes the need for the attribute.

Back to the ConventionController. I suspect a good chunk of the magic has to do with NHibernate being able to discern whether the object is new or updated based on the ID that is passed to it. But even if you don’t use NHibernate, this same technique could be used. I.E. Check if the ID of the object is 0. If so, it’s new. Otherwise, it’s old.

There is a *very* large caveat to this method. If the object you are updating has other properties that are *not* updated on this page, they will be set to whatever default value is appropriate for its datatype. And when it is then sent to the database for "updating", its corresponding field will be updated to this value right alongside every other field.

In this case, you can add an intermediate step in the process somewhere. I.E. Somewhere along the line you’ll need to do the following:

  1. Retrieve the object from the database (based on the ID)
  2. If an object is retrieved, update it with the values that were entered in the view.
  3. Save the object

This way, you won’t overwrite any properties with default values just because they aren’t editted in this particular view of the object. In theory at least. I haven’t actually tried it myself.

Kyle the Purported

This entry was posted in ASP.NET MVC, Featured. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • Kyle Baley

    @Ben

    Yeah, that occurred to me when I was writing this: “I bet I’ve finally caught up to what Ben was saying a week and a half ago.”

  • http://www.flux88.com Ben Scheirman

    Yeah, Kyle what you just said is what I meant before. If you wanted to extend that into more convention, then you could perhaps make a new [LoadEntity(“sibling”] attribute that would be “aware” of your entity and any repository implementation.

    Then your controller base class could look for this and do

    T entity = Container.Resolve>().FetchById( Request.Form[prefix + “.id” );
    and then pass in entity to the actual method.

    this is what I meant by “it would be too magical”

  • http://www.ayende.com/Blog/ Ayende Rahien

    Yes, that is the way to go

  • Kyle Baley

    If I follow, I think I alluded to this already at the very end. In the Save action, you would need to:
    1) Get the object from NH
    2) Update it with the object created by the controller from the view
    3) Save it again

    This is for the Edit scenario but it could be updated to handle both Edit and Create:

    1) Get the object from NH
    2) If no object is returned, use the one created by the controller from the view
    3) Otherwise, update the object retrieved from NH with the object created by the controller from the view
    3) Save the object (this will create a new one if it didn’t already exist)

    Does that sound closer to correct?

  • http://www.ayende.com/Blog/ Ayende Rahien

    In other words, what you have so far is not yet appropriate, you need to get an instance from NH first.

  • http://www.ayende.com/Blog/ Ayende Rahien

    I am not sure how the convention stuff work, in MR, you do [ARDataBind],. and that takes care of it for you.

  • Kyle Baley

    @Ayende

    I tried this with an extra parameter on the object that I set manually in the database and that doesn’t appear in the view. When I updated it, that property got set to NULL. I’m guessing because the ConventionController is creating a new instance of the object when it calls the action and it is setting only those properties it knows about from the view.

    Is that what you were referring to?

  • http://www.ayende.com/Blog/ Ayende Rahien

    kyle,
    You generally do not just create an object and use it, you try to get it from the DB first, which solve you problem.
    You need to remember the object version, so you wouldn’t have last commit win, but that is easy enough.