Next important part of Composite UI, if not the most significant, is the WorkItem. According to documentation it “a run-time container for components that are working together to fulfill a use case. These components may consist of SmartParts, controllers, services, UIElements, and other components.” So if we look at Outlook as example here, we have use cases like: browsing email, scheduling appointments or looking up contacts. As I understand, these would correspond to main WorkItems of EmailWorkItem, CalendarWorkItem and ContactWorkItem. Of course each of them can have child WorkItems like ComposeEmailWorkItem or EditContactWorkItem. What's more there can be also some cross references like you can call “add contact” use case from “browse emails”.
What is important here is that WorkItem is the only component that has access to all other components mentioned above. Visibility of all other elements to one another is limited and basically all communication goes through the WorkItem. This architecture nicely supports two most common design patterns for moder UI: Model-View-Controller (MVC) and Model-View-Presenter (MVP). The purpose of both patterns is to separate the user interface logic from the business logic. The primary difference between them is that MVC requires events that are exposed from the model to update the view, whereas MVP depends on the presenter to update the view directly. Diagram below compares the two patterns.
Here Forms, SmartParts and user controls play the role of View, and WorkItems hold state and other data that form the Model. To implement the Controller we can derive from the base class with the same name that contains references to owning WorkItem and associated State.
Lets get back to the PetShop sample and try to discover what WorkItems exist here. I would try to group the pages (now SmartParts) into following three functional areas:
- Inventory - Category, Items, ItemDetails, Search,
- Account - EditAccount, CreateAccount, SignIn, SignOut,
- Order - ShoppingCart, Checkout, OrderBilling, OrderProcess, OrderShipping.
I will start with the InventoryWorkItem because it is first thing we would probably use in the PetShop. The Category, Items and ItemDetails views are all similar - they just show bunch of data for given identifier. For first two I would use DataGridView and a BindingSource and on the last one will just throw some labels. The interesting part is how to assign the identifier to the corresponding view. For this I would implement methods in InventoryWorkItem that create the view, put it into the workspace and order it to load data for given identifier. Here is one such method:
public void ShowCategory(string categoryId){
CategoryView categoryView = this.SmartParts.AddNew<CategoryView>(); this.Workspaces["content"].Show(categoryView); categoryView.LoadData(categoryId);
}
Methods ShowProduct and ShowItemDetails are implemented in similar way. But who would call these methods? Looking at the Inventory use case we have a scenario that selecting a product in a category would open a list of items for that product and then selecting one such item would show its details. Therefore I would turn one of the grid columns on both views into links and add handlers for CellContentClick event. But how do I access the WorkItem from the view? From the above diagrams I see that for this I should create a Controller. For now it would simply forward my calls to the InventoryWorkItem.
public class InventoryController : Controller{
public new InventoryWorkItem WorkItem {
get { return base.WorkItem as InventoryWorkItem; } }
public void ShowProduct(string productId)
{
this.WorkItem.ShowProduct(productId);
}
public void ShowItemDetails(string itemId)
{
this.WorkItem.ShowItemDetails(itemId);
}
}
Now I only need to create instance of this Controller for each view. We can do it quite easily with the power of dependency injection:
private InventoryController _inventoryController;
[
CreateNew]public InventoryController InventoryController{
get { return _inventoryController; } set { _inventoryController = value; }}
The CreateNew attribute instructs the ObjectBuilder to instantiate and initialize the InventoryController when the SmartPart is created. How convenient.
One last thing is to actually create and Run the InventoryWork item. I would do this whenever user selects one of the categories from the default page or the navigation bar. And now I realize that I also need a root WorkItem to maintain the main page and other WorkItems. I create a ShellWorkItem and change the declaration of ShellApplication class. Now I can also move the code that loads the default page from application's AfterShellCreated to ShellWorkItem's OnRunStarted method. Finally, here is the code to run the InventoryWorkItem when user selects a category:
public void ShowCategory(string categoryId){
InventoryWorkItem inventory = this.WorkItems.AddNew<InventoryWorkItem>("inventory"); inventory.Run();
inventory.ShowCategory(categoryId);
}
Ok. Now I will try to implement remaining views using what I've learned already. I won't be boring you with details until I start to use some new elements.