There is a dirty little secret in the Grid market: making a Grid Editable, on a row-by-row basis (not in its entirety), is a nightmare.
So, as we all know, MVVM is the cure-all for everything UI, so apply MVVM to the Grid form and away we go. Right?
You see, hosting the Grid in the View of a form actually ends up making the hassle even worse. But do not despair, there is a simple-enough solution to this otherwise complex dilemmas. I call this solution: the RowViewModel.
The what you say? Heretical mishmash you say? Read on fearless coder, read on.
First, let’s at least agree to some nomenclature. Failing that, here is MY understanding of MY use of these terms.
View – An unintelligent skin hosting UI controls and bindings and minimal code to call Commands and raise Events. Can be injected into the View Model at run time.
View Model – All the intelligence behind the View is stored here. This client side class defines and executes Commands and knows how to respond to events and manipulate UI controls and fields. This class will pass on all persistence methods to the repository class. The ViewModel holds a reference to an instance of the client side business object and wraps its properties in ViewModel properties for binding to the UI. The UI does not bind directly to the business object, but to the ViewModel property instead. For example, suppose you can’t save a customer until the Name, Phone and City are populated. The ViewModel owns the state of the Save button (enabled/disabled) and the validation flags for required fields. It makes sense to put a function in each property setter in the ViewModel for Name, Phone and City that will check whether to enable/disable save.
Repository – A class that knows how to interact with your Data Layer and is responsible for dealing with the persistence methods provided in your ORM framework. (For example, DevForce has an EntityManager that has a Save method. The Repository is the only class that should ever call that Save method.)
Business Object – In DevForce 3.5, there’s a client side Silverlight business class that is the synonym of the server-side data class.
Data Object – In DevForce 3.5, the server-side C# version of the business object. These classes are generated by DevForce from the EntityFramework model.
RowViewModel Conceptual Overview
So here’s the application model for a UI Module with an editable grid. As an example, let’s imagine we want to provide an editable heirarchical grid with a list of customers in it and a list of their addresses nested underneath each customer row.
First, we need the View and ViewModel for the grid form.
1. One View class for the page/tab/control that hosts the grid (e.g. CustomerGridView).
2. One ViewModel class backing the page/tab/control that hosts the grid (e.g. CustomerGridViewModel).
The gridView and gridViewModel are responsible for managing interactions around the grid and for actions that affect multiple rows. For example, "Select All", "Print All", "Expand All", "Print Selected Customers", Filters of all sorts, column sorting, etc.
Second, we need:
3. A RowViewModel class for each row type (e.g. CustomerRowViewModel, AddressRowViewModel).
The gridViewModel holds a collection of the top level <parent>RowViewModels (e.g. CustomerRowViewModels). Each top level RowViewModel holds a collection of <child>RowViewModels (e.g. AddressRowViewModels).
Lastly, there should be:
4. One Repository class to support the functionality represented by the entire page. This repository is likely inherited from a utility class that provides all the standard repository behavior (persistence, querying, etc.) using generics.
Now that you have been introduced to these classes, here’s how they come together:
Say we have Customers and Addresses.
The Grid itself will be configured as a hierarchical grid with one parent band and one child band. Where the parent shows each Customer as a row and each Address as a row underneath the Customer row. Traditionally, you would bind the parent row source to the Customers collection property of the gridViewModel, and the child row source to the Addresses property of the Customer object.
Now, imagine the user starts to edit a cell. Which cell? Well, the grid will tell you which cell. It’s the Customer Rep column for Row 17. Which customer is bound to Row 17? Some grids won’t tell you, so you have to jump through hoops to figure it out. And heaven forbid the grid is sortable because with one click, all your mappings are out of date. Meanwhile, back to the Customer Rep column. It’s bound to a combo box. You have three types of customers: Volume, Retail, and Educator. There are different Reps for each type. Uh-oh. Now you need, not just any combo box, but a filtered combo that is dependent on the Customer Type (which we’ll assume was selected earlier in the row).
If you use the Grid’s built-in combo box column type, it is bound to a collection of Customer Reps in the gridViewModel and you can watch as you filter that list and the grid blanks all the rows that have a value that is not in the filtered list. Yeuch.
Or you can try a hovering combo that comes to the exact co-ordinates of the cell and hovers above it providing the illusion that it is part of the grid, then you can populate it however you want. But wait, that means you now have to write all the code to get that combo to work, manage the binding to the instance you need, and get the whole thing to sync properly when you’re done editing it. Ack! Oh, and you can’t use the grid’s built-in undo feature any more.
What to do? Enter the RowViewModel.
Here’s how we bring the RowViewModel into this.
In the parent band, each grid row is a single View for a single RowViewmodel in the gridViewModel.CustomerRowViewModels collection.
Each CustomerRowViewModel holds a collection of its own AddressRowViewModels.
The child bands (there is one child band for each parent band that has children), have rows, each row in a child band is a View for an AddressRowViewModel.
In this structure, the rows can be bound appropriately and each RowViewModel is responsible for the interactions with 1 and only 1 row in the grid.
Back to our Customer Rep combo box problem. Now, the CustomerReps collection is a property of the CustomerRowViewModel and the grid’s built-in combobox can be happily bound to the CustomerRowViewModel.CustomerReps collection without complaint. When the Customer Type changes, the CustomerReps collection is repopulated and the binding will update the combo box automagically. Yay. No code to translate from Cell to Customer and the tie-in from the CustomerGrid Column binding to the CustomerRowViewModel property is a very standard binding.
So how does this look in code? I’m going to have to get back to you on that as I put together the code example for this simple scenario.