A recent Microsoft Codeplex forum interaction had me updating the SDMS application so that the Employee Module and all of it's views were Multi-targeting (worked on both Desktop and Silverlight). The reasoning follows below:
[TestMethod]
public void TestMethod1()
{
// IModule does all of the heavy lifting - configures all interfaces
// so we'll just use it to set things up.
IModule module = Container.Resolve<IModule>();
module.Initialize();
// Resolve the EmployeeList Presenter
EmployeeListPresenter MockView = Container.Resolve<EmployeeListPresenter>();
// Give WCF Service a chance to complete
Thread.Sleep(2000);
// Cast so we can easily access presentation model
EmployeePresentationModel model =
(EmployeePresentationModel) MockView.Model;
Assert.AreEqual(3, model.EmployeeList.Count,
"Employee list should have three records!");
}
Note we can put the Testing thread to sleep :) I just tested everything short of the UI which is databound to the presentation model (nothing to test in the view) all the way through the WCF Service and back. Since my PresentationModel implements INotifyPropertyChanged I can rest assured my View will work (assuming I did my Binding correctly).
Let's see what IModule was up to (showing the effectiveness of multi-targeting)
public class ModuleEmployee : IModule
{
// For class use
private readonly IUnityContainer container;
private readonly IRegionViewRegistry regionViewRegistry;
/// <summary>
/// Constructor : Setup class
/// </summary>
/// <param name="container"></param>
/// <param name="regionViewRegistry"></param>
public ModuleEmployee(IUnityContainer container,
IRegionViewRegistry regionViewRegistry)
{
this.container = container;
this.regionViewRegistry = regionViewRegistry;
}
public void Initialize()
{
RegisterViewAndServices();
// EmployeeModule - Views folder
regionViewRegistry.RegisterViewWithRegion("MainRegion",
() => container.Resolve<EmployeeMainPresenter>().View);
regionViewRegistry.RegisterViewWithRegion("frmCaption",
() => container.Resolve<frmCaptionPresenter>().View);
regionViewRegistry.RegisterViewWithRegion("frmEmployeeList",
() => container.Resolve<EmployeeListPresenter>().View);
regionViewRegistry.RegisterViewWithRegion("TabInformation",
() => container.Resolve<EmployeeInformationPresenter>().View);
regionViewRegistry.RegisterViewWithRegion("TabAssigned",
() => container.Resolve<EmployeeAssignedPresenter>().View);
regionViewRegistry.RegisterViewWithRegion("TabInWork",
() => container.Resolve<EmployeeInWorkPresenter>().View);
regionViewRegistry.RegisterViewWithRegion("frmStatus",
() => container.Resolve<frmStatusPresenter>().View);
}
private void RegisterViewAndServices()
{
container.RegisterType<IEmployeeMainView, EmployeeMainView>()
// Layers
.RegisterType<IEmployeeProviderBLL,EmployeeProviderBLL>()
.RegisterType<IEmployeeProviderDAL,EmployeeProviderDAL>()
// Views
.RegisterType<IfrmStatusView, frmStatusView>()
.RegisterType<IfrmCaptionView, frmCaptionView>()
.RegisterType<IEmployeeListView, EmployeeListView>()
.RegisterType<IEmployeeListView, EmployeeListView>()
.RegisterType<IEmployeeInWorkView, EmployeeInWorkView>()
.RegisterType<IEmployeeAssignedView, EmployeeAssignedView>()
.RegisterType<IEmployeeInformationView, EmployeeInformationView>()
// Services
.RegisterType<IEmployeeService, EmployeeService>()
// Models
.RegisterType<IEmployeePresentationModel, EmployeePresentationModel>(
new ContainerControlledLifetimeManager());
}
}
}
It did some pretty heavy lifting which tells me everything that will be executed during Silverlight runtime - works.
The following is the Presenter, which is responsible for updating the Presentation Model (which the view is observing). You can see that my Desktop Unit Test effectively exercises many, if not all, logic within the process.
Note: Silverlight unit testing is done in a browser... I'd rather take this approach.
Hope this helps in your quest to finding an architecture that works for you!
Bill
public class EmployeeListPresenter : PresenterBase<IEmployeeListView>
{
readonly IEmployeeService employeeService;
readonly IEventAggregator aggregator;
readonly IEmployeePresentationModel model;
/// <summary>
/// Constructor : setup class
/// </summary>
/// <param name="container"></param>
/// <param name="view"></param>
public EmployeeListPresenter(
IEmployeeListView view,
IEmployeePresentationModel model,
IUnityContainer container,
IEventAggregator aggregator,
IEmployeeService service) : base(view,model,container)
{
this.aggregator = aggregator;
this.employeeService = service;
this.model = model;
// Subscribe to ListBoxChanged event and
aggregator.GetEvent<ListBoxChangedEvent>()
.Subscribe(ListBoxChangedEventHandler, true);
aggregator.GetEvent<EmployeeEvent>()
.Subscribe(EmployeeEventHandler, true);
// Async call to service to populate employee list.
// The EmployeeListEventHandler will be called when
// data is received
employeeService.GetEmployeeList();
}
/// <summary>
/// Subscribed to in constructor - updates the model's
/// SelectedEmployee property every time a new employee is selected
/// </summary>
/// <param name="args"></param>
private void ListBoxChangedEventHandler(SelectionChangedEventArgs args)
{
model.SelectedEmployee = args.AddedItems[0] as Employee_Data;
StatusBarEvent sbEvent = aggregator.GetEvent<StatusBarEvent>();
if (sbEvent != null)
aggregator.GetEvent<StatusBarEvent>().Publish(
new StatusBarData
{
Message = string.Format("You clicked {0}",
model.SelectedEmployee.DisplayValue),
Panel = StatusPanel.Left
});
}
/// <summary>
/// Handler for when Employee list is returned by service call to
/// GetEmployeeList()
/// </summary>
/// <param name="args"></param>
private void EmployeeEventHandler(EmployeeEventArgs args)
{
model.EmployeeList = args.EmployeeList;
}
}