Friday, June 20, 2008

CAB/SCSF: View Parameters

CAB/SCSF use the wellknown Model-View-Presenter design pattern for its views. There is, however, a design flaw in its implementation: you cannot pass parameters to the view constructor. As documenten in the SCSF knowledge base, the proven practice is to pass the parameters using the WorkItem State collection.

This proven practice can be improved using generics to become type-safe, while still adhering to the MVP design rules. Add the following new method to the WorkItemController base class in the Infrastructure.Interface project:

public virtual TView ShowViewInWorkspace<TView, TParams>(string viewId, string workspaceName, TParams viewParams)
where TParams : class
{
string stateKey = StateItemNames.PresenterConstructorParams + typeof(TView).FullName + ":" + typeof(TParams).FullName;
_workItem.State[stateKey] = viewParams;

TView view = ShowViewInWorkspace<TView>(viewId, workspaceName);
return view;
}

Then add the following new method to the Presenter base class in the Infrastructure.Interface project:

protected TParams ViewParameters<TParams>() where TParams : class
{
//NOTE: must use typeof(view instance) to get correct run-time type of generic view type
string stateKey = StateItemNames.PresenterConstructorParams + _view.GetType().FullName + ":" + typeof(TParams).FullName;
return (TParams)_workItem.State[stateKey];
}

Now you can simply pass the parameters from the module controller to the view using ShowViewInWorkspace and then get the parameters in the presenter's OnViewReady event:

_viewParameters = ViewParameters<ViewParameters>();
if (_viewParameters == null)
{
throw new ArgumentException("Required view parameter error", "ViewParameters", null);
}

This little design improvement hides the gory details of using the untyped object State collection, e.g. generating shared keys and type casting.

3 comments:

Cox said...

Nice Snippet! But how can I handle multible View/Presenter-Pairs in a WorkItem with it? For example when I've got a WorkItem for editing a contract entity with two addresses (once for the service and once for billing), but there is only one view type I can use for addresses.

...
this.serviceAddressView = this.ShowViewInWorkspace<AddressView, Address>("ServiceAddressView", "TabWorkspace", Contract.ServiceAddress);

this.billingAddressView = this.ShowViewInWorkspace<AddressView, Address>("BillingAddressView", "TabWorkspace", Contract.BillingAddress);

...

Kjell-Sverre Jerijærvi said...

How about adding the viewId to the stateKey in the setter? The view ID should be available in the Presenter base class through the _view member for use in the getter.

Cox said...

Maybe like this in the getter?

protected TParams ViewParameters<TParams>()
where TParams : class {
if (this.workItem == null)
throw new InvalidOperationException("WorkItem not set");

if (this.view == null)
throw new InvalidOperationException("View not set");

string stateKey = string.Empty;

foreach (KeyValuePair<string, object> kvp in this.workItem.SmartParts)
if (this.View == kvp.Value) {
stateKey = kvp.Key;
break;
}

if (string.IsNullOrEmpty(stateKey))
return null;

return (TParams)this.workItem.State[stateKey];
}