Disclaimer
This has nothing to do with tying-up anorexic runway models, so if you came here looking for photos of young women in handcuffs, you’re in the wrong place. No, today I want to talk about one of my favorite features of the ASP.Net MVC web framework: Model Binding, and how – even with all its sugary goodness – it can still bite you in the ass, as it did me this week.
The App
The application I’ll be describing is an ASP.Net MVC 2 app using the .aspx view engine, and it is used by lab technicians to track the position of specimens (which department, which refrigerator, which rack, and which hole in the rack where the specimen is placed) so that they can easily find specimens when they need them. This may sound boring, but it’s not; imagine, for instance, that your blood sample gets mixed up with someone else’s and your test comes back HIV positive. Not good. The system design is simple, but it simply has to work.
Within the lab, different departments use different rack layouts:
Rack Type | Number of Rows | Number of Columns | |
---|---|---|---|
Chem | 6 | 12 | |
Hematology | 9 | 12 | |
Micro | 5 | 8 | |
Funny | 6 | 12 | |
Virology | 12 | 10 | |
Receiving | 6 | 12 |
So when a rack is created, it is given a certain rack type. The system looks at this property in order to determine how the specimens are laid out on the screen. A Chemistry rack has 6 rows and 12 columns; Hematology uses 9 x 12, etc.
I was smart enough to add a “catch-all” bit of logic in my Controller EditSpecs action method to deal with a missing or unknown rack configuration, which could occur if someone accidentally deleted a rack type:
public ActionResult EditSpecs(int id, string specnum) { List<Spec> SpecsForRack = (_ctx.Specs.Where(s => s.RackID == id)).ToList(); var currRack = _ctx.Racks.Single(r => r.ID == id); if (currRack.RackType == null) { //Stranded rack for which the type has been deleted. //Set arbitrary rows and cols. ViewData["cols"] = 8; ViewData["rows"] = 8; ViewData["RackName"] = currRack.Rack1;
This code is pretty straightforward: Before displaying the EditSpecs view, I look up the rack type for the current rack. If it doesn’t have a type (e.g. currRack.RackType is null), then I set the rows and columns to 8. My thinking when I wrote this was that this was a “one-chance-in-a-million” issue, and would (probably) never occur. Oh yeah, and I didn’t write a unit test for it. I must now wear the Cone of Shame.
The Go-Live
So Wednesday at 8am the application went into production; overall, the day progressed smoothly enough; there were some complaints, but mostly they were “settling in” issues, where people were getting used to a new system. No data integrity problems, no Yellow Screens of Death, not even a 404!
I was ready to wrap up my day around 4pm when the phone rang. It was the lab. There were two Hematology racks (9×12) which were only showing the first 64 specimens. My heart leapt into my throat. There should be 108 specimens, the Lab Supervisor explained; they entered 108 specimens when they created the rack. But now they only saw 64.
Now, if you know your times tables, you know that 8 x 8 = 64, and I should have immediately realized what had happened. But “fear is like a giant fog. It sits on your brain and blocks everything…” so the very first thing I did was to fire up SQL Management Studio and run a query to see if the data was actually there.
Sweet relief. It was. That meant that I was probably dealing with a display bug, so I went to the code. It was then I saw the code above; a quick run through the debugger showed me that the rack in question had no rack type, and was getting the arbitrary 8×8 layout configuration. I had two realizations at the same time: 1.) Eight rows and columns was a really, really stupid selection since I knew I had rack configurations larger than that when I built the damn system, and 2.) I had a bug somewhere that was allowing racks to exist without a rack type.
So, I did quick SQL query to determine how many racks were missing their Rack Type and found (fortunately) only 3. I quickly added the appropriate Rack Type to each so the folks in the Lab could get to work, and then started looking through my code.
Now, the fact that there were just three racks affected gave me some more clues. There were twenty-plus additional racks that were fine, so I was relatively confident of my Rack Creation code. No, the problem had to be somewhere else. So I created a test rack, added some fake specimens to it and checked the database again. It was fine, carrying the correct Rack Type. OK, so that wasn’t it. Then I edited the rack configuration; Mind you, I didn’t change anything. I just brought the “Edit Rack” view and hit Save.
Guess what? The Rack Type disappeared from the record in the database.
And that brings me back around to Model Binding…
Model Binding
Model Binding is one of the reasons I love to code with ASP.Net MVC. No more querystrings and session variables (well, not usually); getting data from an HttpPost event is trival. To wit, let’s look at my Edit() Controller Action Method:
public ActionResult Edit(int id) { var currRack = _ctx.Racks.Where(r => r.ID == id).FirstOrDefault(); ViewData["locs"] = GetLocations(); ViewData["frigs"] = GetFrigs(); ViewData["sects"] = GetSects(); return View("Edit Rack", currRack); } [HttpPost] public ActionResult Edit(int id, Rack editRack) { try { Rack sourceRack = _ctx.Racks.Single(r => r.ID == id); _ctx.ApplyCurrentValues(sourceRack.EntityKey.EntitySetName, editRack); _ctx.SaveChanges(); } catch (Exception ex) { throw ex; } return RedirectToAction("Index"); }
The model binding comes into play on the postback of the Edit view. Note that the action method takes two arguments: An integer representing the ID of the rack that was edited and a Rack object with all the edited info. I use the ID to get the source rack from the database, and then use the DBContext.ApplyCurrentValues method to update it with the info gathered from the Edit view, now stored in the editRack object (the second argument).
This is so much sweetness to an old guy like me. I don’t have gather a bunch of form (or QueryString) variables (which will be delivered as strings) and cast them to the correct type, test for empty/null strings (using the string.IsNullOrEmpty extension method makes this much simpler but still…), individually store each variable into the correct property, etc. etc. ASP.Net MVC does all that for me; all I have to do is set the argument to the appropriate class, and it auto-magically fills that up with the editing information, ready for shoving into the database.
But with great power, comes great responsibility, as Uncle Ben would have told us. When using model binding, if you exclude a property from the view and update your object via DBContext.ApplyCurrentValues(), then the value of that property will be null (in the case of RackTypeID — which is an int — that’s 0)!
Guess which property I had not included on my Edit Rack view? You guessed it, Rack Type! So whenever a rack was edited, the Rack Type was being nulled out. The system could no longer tell what kind of rack was in use and therefore defaulted to the (again, very stupid) 8×8 layout configuration, hiding any values above 64.
A simple fix then: I just included the property in my Edit Rack view, thusly:
<div class="editor-label"> <%: Html.LabelFor(model => model.RackTypeID) %> </div> <div class="editor-field"> <%: Html.DropDownListFor(model => model.RackTypeID, (IEnumerable<SelectListItem>)ViewData["racktypes"], new { @class = "notDefault" })%> <%: Html.ValidationMessageFor(model => model.RackTypeID)%> </div>
So, two bottles of water, one Lorazapam and one git push and web deploy later and all was well. Lesson learned. And so, Grasshopper, learn from my mistakes…and heed my warning: Write (good) unit tests! And always make sure your properties are properly bound to your model!
‘Till next time, happy hacking, folks!
Brilliant thought