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:
- Retrieve the object from the database (based on the ID)
- If an object is retrieved, update it with the values that were entered in the view.
- 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