I came across a class the other week called TestableRootWorkItem. I was put onto it from a post I made on the Smart Client forum, and it can be found in the Appraisal Work Bench test module.
Basically it's a WorkItem which is initialised with a core set of services (of your choosing) and can then be used to test your own CAB code.
Example time - imagine you've a presenter / view combo called ShowCustomersPresenter.
The view implements an interface called IShowCustomersView.
In your test fixture you can use the TestableRootWorkItem a little bit like this:
public void MyTestOfWhatever()
{
TestableRootWorkItem rootItem = new TestableRootWorkItem();
rootItem.Services.AddNew<ICustomerService, MockCustomerService>();
ShowCustomersPresenter presenter = rootItem.Items.AddNew<ShowCustomersPresenter>();
IShowCustomerView mockView = new MockView();
presenter.View = mockView;
presenter.FindCustomers();
Assert.AreEqual(3, mockView.CustomerCount);
}
so, what's going on here then?
Well, we start with adding a mock customer service to the work item.
Next we add an instance of the Presenter we want to test. ObjectBuilder will spot a [ServiceDependency] attribute looking for a service of type ICustomerService and will add my MockCustomerService which has been registered.
The presenter's view is an IShowCustomerView. I don't want to test the real one so have added a MockView class which implements the interface. I have total control over this, but most importantly it's not a UI component.
here is my implementation of these 2 classes:
public class MockCustomerService: ICustomerService
{
public IList<CustomerSearchResult> FindCustomers(string customerName)
{
return new List<CustomerSearchResult>(new CustomerSearchResult[] { ... });
}
}
public class MockView: IShowCustomerView
{
public int CustomerCount = 0;
public ShowCustomers(IList<CustomerSearchResult> results)
{
CustomerCount = results.Count;
}
}
it's pretty plain sailing from here.
I simulate someone clicking the Find button by calling presenter.FindCustomers() directly.
My implementation of the Presenter under the hood calls the ICustomerService service to get the customers, and then calls View.ShowCustomers(results).
Using a well known service, and a mock view I can prove in the test that indeed this is what is happening.
To make it more interesting it's pretty straight forward to test Presenters raising or subscribing to events. You can create a MockEventSubscriber class which uses a CAB [EventSubscription(....)] to check for the raising of an event. The test method just has to add this class directly to the WorkItem (WorkItem.Items.AddNew<MockEventSubscriber>()) and you're there!
CABs loose coupling event plumbing makes it perfect for writing classes that can be tested in isolation. In-fact, I think that if you're not testing CAB code in this way that you're really missing a trick! I'm about to start on the next phase of a CAB project and hope to report back on how it all goes!
That's all for now :o)
Sunday, May 27, 2007
Sunday, May 06, 2007
CAB / Smart Client Software Factory pointers
I've been involved in a project which is using Microsofts Component Application Block for a few months now and thought I'd drop an entry on CAB 101 - do's and donots!
CAB is heavily based around MVP and the idea of splitting use-cases into a WorkItem. Sounds complicated? Don't let it. A WorkItem is nothing more than a container of things which are required to get the job of a use-case done.
So, a workitem may contain some views, some services, and some logic for co-ordinating them together. Simple huh?
Lets look at a use case of searching for customers.
What views are involved in this? For this example lets say we have a view to enter search text, and a view to show a list of customers.
Get your GUI guys to knock up these 2 views. Make them look blinding! Just hold off on the actual guts of what is going on as it's not their job!
GUI guys working on the views? Great. Let's begin the meaty stuff.
Lets create a class that will hold all of the parts involved in the use-case together. This is will be subclass of WorkItemController.
Override the Run() method on this type. Lets have it create a Search Criteria view and add it as SmartParts to the WorkItem's SmartPart collection.
SearchCriteriaView view = WorkItem.SmartParts.AddNew();
Let's also show this view in the main workspace.
WorkItem.Workspaces[MainWorkspace].Show(view);
Assume that the view has some fields for entering search criteria, and a Search button. When the user clicks on the Search button we'll delegate to our SearchCriteriaViewPresenter class (remember MVP)...
The Presenter for the Search Criteria screen is the thing that will go and find the results. But how does it do this?
This is where we use another piece of CAB goodness - its DependencyInjection framework. CAB will wire up objects we want, setting properties on them by magic. Did I mention that we have already asked CAB to register a CustomerSearch service on our behalf? Nope? Well we did!
In the Search Criteria presenter we'll have some lines that look like this...
[ServiceDependency]
public ICustomerSearch
{
set { _customerSearch = value; }
}
public void Search(....)
{
IList<SearchViewCustomer> customers = _customerSearch.Search(....)
}
But how do we get these results to the results view? This is where CAB's idea of loose coupling comes into play. What we'll do is add the results into the WorkItem's Items Collection and then raise an event to say we've found some search results. The Search method becomes more like this.
[EventPublication("ResultsReturned", PublicationScope.WorkItem)]
public event EventHandler<EventArgs> ResultsReturned;
public void Search(....)
{
IList<SearchViewCustomer> customers = _customerSearch.Search(....)
WorkItem.Items.Add(customers, "CustomerList");
ResultsReturned(this, EventArgs.Empty);
}
Woah! What's going on there. Well the EventPublication line instructs CAB that this class can raise a CAB event. CAB events offer more flexibility over .net events. We can change the scope of who they are raised to. In our case we're going to tell the WorkItem that we've found some results.
Go back to the WorkItemController from earlier and add the following method.
[EventSubscription("ResultsReturned")]
public void OnResultsReturned(object sender, EventArgs e)
{
ResultsView view = WorkItem.SmartParts.AddNew();
WorkItem.Workspaces["MainWindow"].Show(view);
}
Remember that other gui screen that was being prepared? Get into the Presenter and add a constructor.
[InjectionConstructor]
public ResultsViewPresenter(
[ComponentDependency("CustomerList") IList<Customer> customers] )
{
View.Customers = customers;
}
The InjectionConstructor attribute is a Object Builder attribute that will let us easily inject data into the new Presenter. In this case we are telling builder that the constructor is dependent on an item called CustomerList. It will look for this item in the Items collection and pass it to the constructor for us.
All we have to do is bind the customers list to the view.
Recap! What have we achieved?
We've created 2 smart parts which act together to perform the use case of finding customers.
W've created a WorkItemController which will hold these 2 views giving us a context for working with just the 2 views loosely without them knowing about each other.
We've used a CAB Event to tell anyone interested that we found some customers.
We used CAB's Items collection to share the customers between 2 views without either of them being aware of each other.
And we used the dependency injection system to obtain a reference to a service, and to wire up a Presenter.
Not bad for 10 minutes work!
CAB is heavily based around MVP and the idea of splitting use-cases into a WorkItem. Sounds complicated? Don't let it. A WorkItem is nothing more than a container of things which are required to get the job of a use-case done.
So, a workitem may contain some views, some services, and some logic for co-ordinating them together. Simple huh?
Lets look at a use case of searching for customers.
What views are involved in this? For this example lets say we have a view to enter search text, and a view to show a list of customers.
Get your GUI guys to knock up these 2 views. Make them look blinding! Just hold off on the actual guts of what is going on as it's not their job!
GUI guys working on the views? Great. Let's begin the meaty stuff.
Lets create a class that will hold all of the parts involved in the use-case together. This is will be subclass of WorkItemController.
Override the Run() method on this type. Lets have it create a Search Criteria view and add it as SmartParts to the WorkItem's SmartPart collection.
SearchCriteriaView view = WorkItem.SmartParts.AddNew
Let's also show this view in the main workspace.
WorkItem.Workspaces[MainWorkspace].Show(view);
Assume that the view has some fields for entering search criteria, and a Search button. When the user clicks on the Search button we'll delegate to our SearchCriteriaViewPresenter class (remember MVP)...
The Presenter for the Search Criteria screen is the thing that will go and find the results. But how does it do this?
This is where we use another piece of CAB goodness - its DependencyInjection framework. CAB will wire up objects we want, setting properties on them by magic. Did I mention that we have already asked CAB to register a CustomerSearch service on our behalf? Nope? Well we did!
In the Search Criteria presenter we'll have some lines that look like this...
[ServiceDependency]
public ICustomerSearch
{
set { _customerSearch = value; }
}
public void Search(....)
{
IList<SearchViewCustomer> customers = _customerSearch.Search(....)
}
But how do we get these results to the results view? This is where CAB's idea of loose coupling comes into play. What we'll do is add the results into the WorkItem's Items Collection and then raise an event to say we've found some search results. The Search method becomes more like this.
[EventPublication("ResultsReturned", PublicationScope.WorkItem)]
public event EventHandler<EventArgs> ResultsReturned;
public void Search(....)
{
IList<SearchViewCustomer> customers = _customerSearch.Search(....)
WorkItem.Items.Add(customers, "CustomerList");
ResultsReturned(this, EventArgs.Empty);
}
Woah! What's going on there. Well the EventPublication line instructs CAB that this class can raise a CAB event. CAB events offer more flexibility over .net events. We can change the scope of who they are raised to. In our case we're going to tell the WorkItem that we've found some results.
Go back to the WorkItemController from earlier and add the following method.
[EventSubscription("ResultsReturned")]
public void OnResultsReturned(object sender, EventArgs e)
{
ResultsView view = WorkItem.SmartParts.AddNew
WorkItem.Workspaces["MainWindow"].Show(view);
}
Remember that other gui screen that was being prepared? Get into the Presenter and add a constructor.
[InjectionConstructor]
public ResultsViewPresenter(
[ComponentDependency("CustomerList") IList<Customer> customers] )
{
View.Customers = customers;
}
The InjectionConstructor attribute is a Object Builder attribute that will let us easily inject data into the new Presenter. In this case we are telling builder that the constructor is dependent on an item called CustomerList. It will look for this item in the Items collection and pass it to the constructor for us.
All we have to do is bind the customers list to the view.
Recap! What have we achieved?
We've created 2 smart parts which act together to perform the use case of finding customers.
W've created a WorkItemController which will hold these 2 views giving us a context for working with just the 2 views loosely without them knowing about each other.
We've used a CAB Event to tell anyone interested that we found some customers.
We used CAB's Items collection to share the customers between 2 views without either of them being aware of each other.
And we used the dependency injection system to obtain a reference to a service, and to wire up a Presenter.
Not bad for 10 minutes work!
So long England, hello Australia!
Well it's been a while since I last wrote a blog entry but I have decided to start up again for no particular reason!
I'll probably keep it quite techie this time though. I'm currently working on a WPF / CAB project so I might drop some entries from time to time about these technologies.
I'll probably keep it quite techie this time though. I'm currently working on a WPF / CAB project so I might drop some entries from time to time about these technologies.
Subscribe to:
Posts (Atom)