Freigeben über


Poor man's UI Composition

I've consistently pushed for simplicity on the Prism project. This was evident when we worked on our first feature: UI composition (View from one module is composed of view(s) from other module(s)). In CAB, we developed Workspaces to provide this functionality. However WPF provided its own containment model. Being able to add arbitrary content to UI elements is core to WPF. Before we develop an elaborate solution for gluing cross module UI elements together, it was important to see what WPF provided out of the box.

As I mentioned in a previous post, modules can share services instances using a service locator. If we generalize this a bit, we can use the service locator to share other things such as views, models, and presenters/presentation models. Take a look at the StockTraderRI. The Market module provides implementations of ITrendLinePresenter and ITrendLineView. The Position module has a PositionSummaryView and corresponding PositionSummaryPresentationModel. The PositionSummaryView is composed of several elements, one being a trend line chart view. The trick here is that the Position module doesn't have an implementation of ITrendLineView, instead it relies on the Market module to provide this functionality.

MarketModule.cs:

 protected void RegisterViewsAndServices()
{
    _container.RegisterType<IMarketHistoryService, MarketHistoryService>();
    _container.RegisterType<IMarketFeedService, MarketFeedService>(new ContainerControlledLifetimeManager());
    _container.RegisterType<ITrendLineView, TrendLineView>();
    _container.RegisterType<ITrendLinePresenter, TrendLinePresenter>();
}

This does add a dependency. The module that provides ITrendLinePresenter and ITrendLineView (MarketModule) needs to be loaded prior to the consumer (PositionModule). This does not mean that the PositionModule depends on the MarketModule specifically, but that the PositionModule depends on module(s) that provide implementations of dependent services/views/etc. The PositionSummaryPresentationModel consumes the TrendLinePresenter through constructor injection. All of the constructor parameters are treated as dependencies by the dependency injection container and are resolved by the container and then passed into the constructor.

 public PositionSummaryPresentationModel(IPositionSummaryView view, IAccountPositionService accountPositionService
                                , IMarketFeedService marketFeedSvc
                                , IMarketHistoryService marketHistorySvc
                                , ITrendLinePresenter trendLinePresenter
                                , IOrdersController ordersController
                                , IEventAggregator eventAggregator)
        {
        View = view;
...
        View.Model = this;
        _trendLinePresenter = trendLinePresenter;
        View.ShowTrendLine(trendLinePresenter.View);

        //Initially show the FAKEINDEX
        trendLinePresenter.OnTickerSymbolSelected("FAKEINDEX");
...
        }

The PositionSummaryPresentationModel's constructor takes an IPositionSummaryView, sets itself into the Model property of the view, and then sets the trendLinePresenter's view into the IPositionSummaryView. The PositionSummaryPresentationModel can then call methods on the trendLinePresenter such as providing its initial state.

Basically what you have here is 1) basic cross module communication and 2) a module composing its view by consuming views/presenters/presentation models from other modules. This is often referred to as "Pull" composition, where resources are being pulled and aggregated together. The "composer" uses the service locater to discover resources and then assembles them together into a reasonable UI. This method does not rely on anything other than WPF's built in composition model and a service locater. Later, I'll talk about how we support a "Push" style composition with Regions.

Comments