Login | Register   
Twitter
RSS Feed
Download our iPhone app
TODAY'S HEADLINES  |   ARTICLE ARCHIVE  |   FORUMS  |   TIP BANK
Browse DevX
Sign up for e-mail newsletters from DevX


advertisement
 

Build Composite WPF Applications : Page 3

Build loosely-coupled, maintainable WPF applications using the Composite Application Guidance patterns and practices.


advertisement
Defining the Shell and Regions
The Shell is nothing more than a normal WPF window class, and will contain the layout controls that determine the overall layout structure of your top level window. It will typically also define named regions, which are injection points where modules can put their views when they load up. The process of injecting a view is done through the region manager service provided by CAL. You'll see the code for doing that later. For now, just focus on how to define the regions.

The appointment manager needs a simple top and bottom pane layout, with appointment viewers presented in the top pane, and appointment editors presented in the bottom pane. The Shell.xaml code looks like this:

<Window x:Class="AppointmentManager.Shell"...> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <ContentControl Grid.Row="0" cwpf:RegionManager.RegionName= "{x:Static infra:AppointmentConstants. AppointmentViewsRegion}"/> <TabControl Grid.Row="1" cwpf:RegionManager.RegionName="{x:Static infra:AppointmentConstants. AppointmentEditorsRegion}"/> </Grid> </Window>

 
Figure 3. AppointmentManager Application and Regions: The top and bottom regions are outlined in red.
A content control defines the top pane, into which modules can inject viewer views, while the bottom pane is a TabControl into which modules can inject edit views. The RegionManager class's RegionName property identifies the regions. The string names of the regions are factored out to constants in the infrastructure library because they are a shared contract of sorts between the shell and the modules; the modules will also need to refer to those constants. That's preferable to repeating the string in many places, because the compiler won't catch any errors if you get one or more of them wrong.

Figure 3 shows the resulting UI, with the views that have been loaded into the regions by the modules already visible. The regions are outlined in the figure.

Loading the Modules
The module loader service in CAL handles loading the modules and calling an initialization method in the module classes. But before it can do that, the module enumerator gets called to determine what modules the application is composed of. Remember, that the appointment manager bootstrapper constructed the ConfigurationModuleEnumerator. Now, you need to specify to the enumerator which modules to load through the app.config file. First you need a config section that points to the CAL class, which knows how to deserialize the module information:

<configuration> <configSections> <section name="modules" type="Microsoft.Practices.Composite.Modularity. ModulesConfigurationSection, Microsoft.Practices.Composite"/> </configSections> ...

Next, you need to specify the module information. As mentioned previously, modules provide the core functionality for the application. This sample has two modules, one that provides the appointment viewing capability (Appointment.Modules.Viewers), and one that provides the appointment editing capability (Appointment.Modules.Editors). They are class library projects that include references to the CAL libraries, and they contain a single module class definition each. Here's the configuration:



<modules> <module assemblyFile= "Appointment.Modules.Viewers.dll" moduleType="Appointment.Modules.Viewers. AppointmentViewersModule" moduleName="ApptViewersModule"/> <module assemblyFile= "Appointment.Modules.Editors.dll" moduleType="Appointment.Modules.Editors. AppointmentEditorsModule" moduleName="ApptEditorsModule"/> </modules> </configuration>

Each module specifies its assembly information, the type of the module class within that assembly, and the name of the module. The name can be used to specify inter-module dependencies by putting a nested collection of dependencies (not shown) under each module element. There is also a property you can set to tell the module loader to defer loading the module until an on-demand call is made to the module loader to load the module later.

Modules
A module class is just a class that implements the IModule interface defined by CAL:

// C# public class AppointmentViewersModule : IModule { public void Initialize() {} } ' VB Public Class AppointmentViewersModule Implements IModule Public Sub Initialize() Implements _ IModule.Initialize ... End Sub End Class

The module performs all its startup code in the Initialize method, similar to the bootstrapper for the Shell. Typically, this first involves registering types with the container that the module contributes, and then setting up any views and showing them through the region manager.

For example, Listing 1 shows the full implementation of the AppointmentViewersModule class. The class has a parameterized constructor so the Unity container can inject its dependencies, which include the container itself and the region manager. The Initialize method first calls a helper method called RegisterViewsAndServices. You don't have to include this, but almost any module needs to do something along those lines, so I usually factor it out into a method of this name as shown. In this case, the only type registered is the AppointmentListView type, based on its interface.

The Initialize method then resolves the view model (which I'll talk more about shortly) and gets an IRegion reference from the region manager service. It then adds the view from the view model into that region and activates it. This process is what makes the view show up in the Shell region defined earlier.

Defining Views and View Models
The easiest way to define a view for a composite application is via a user control. For example, in the Appointment.Modules.Viewers library, the AppointmentListView user control contains the grid that is presented in the top portion of the window. You could just put all your view logic in the code behind of the user control, but that is not a great idea from a separation of concerns perspective, and will reduce the testability and maintainability of your views.

Instead, you should use one of several popular UI composition patterns, such as Model-View-Controller, Model-View-Presenter, or Model-View-ViewModel. The appointment manager sample application uses the latter, but combined with Model-View-Controller in the editing module. Model-View-ViewModel is also known as Presentation Model, but ViewModel is a popular name for this pattern in the context of WPF.

The idea behind view models is that you define a class called a ViewModel that is similar to a presenter in Model-View-Presenter. This class sits between the view and the model (domain model). The main purpose of the view model is to offer up data to the view in the way the view wants to see it. In other words, the view model should expose properties that make hooking up the UI elements of the view easy using simple data binding expressions. The ViewModel might also expose commands as properties that the view can bind to.

By leveraging data binding in WPF and defining your UI logic in a ViewModel class, you can usually keep your code behind file empty of any code except the constructor and InitializeComponent call required for proper XAML construction. You might have to occasionally handle an event from a UI control in the code behind of the view (you can see an example in the AppointmentListView class), but if so, you should immediately dispatch that call into the ViewModel to keep all the real handling logic in the ViewModel or other non-view classes such as a controller. Doing this makes it easier to unit test the logic in your application. In fact, by separating your logic code from your view definition in this way, you can even define views in WPF with just a combination of a data template defined in a resource dictionary and the ViewModel that it binds to.

Listing 2 shows the view definition for the AppointmentListView user control. You can see it contains a ListView control that is bound to an Appointments collection property, with each of the columns specifying a DisplayMemberBinding to a property on the contained items in that collection. Thanks to the data binding mechanisms of WPF, you don't have to specify in the view exactly where those properties are coming from or the type of the bound data object. This keeps the view nicely decoupled from the underlying data, which will be provided by the ViewModel. Also notice that there is a context menu hooked up to an edit command in the view. You'll see more about Composite WPF commands in a little bit, but this shows the hookup for one of those commands.

Listing 3 shows the ViewModel for the appointment list view. Note that the pattern used here makes the ViewModel responsible for constructing its view through dependency injection in its constructor, and it exposes that view as a public property. This makes it possible for the module (see Listing 1) can construct the ViewModel, but can still get access to the view reference to add it to a region. The view class implements the IAppointmentListView interface, which defines an event that the ViewModel can listen to for user selections in the list. You can look at the download code for the full details on that aspect. The ViewModel also sets itself as the DataContext for the view. This allows the view to data bind to the properties exposed by the view. The Unity container constructs the AppointmentListViewModel class, and it also takes care of injecting any other dependencies that the class has (as specified by its parameterized constructor). These include the view itself, the appointment data service, and the event aggregator service (covered later).

Another important part of the design of a ViewModel is that for all properties exposed to the view for data binding, you need to make sure those properties raise change events to the view, so that the view can stay up-to-date with the underlying data. You can do this one of two ways. You can implement INotifyPropertyChanged on the ViewModel as shown in Listing 3 and raise the PropertyChanged event in the set block of each property, or you can derive the ViewModel from DependencyObject and implement the properties as DependencyProperties. The sample project uses the former, because the implementation is a little more straightforward. Implementing the properties as DependencyProperties might make sense if you were going to animate those properties in some way, because you can animate only DependencyProperties in WPF.

Adding Views to Regions
To add a view to a region, you just need a reference to the region manager service and an instance of the view. If you look again at the module in Listing 1, you'll see these lines of code in the Initialize method:

// C# IRegion region = m_RegionManager.Regions[ AppointmentConstants.AppointmentViewsRegion]; region.Add(viewModel.View); region.Activate(viewModel.View); ' VB IRegion region = m_RegionManager.Regions( _ AppointmentConstants.AppointmentViewsRegion) region.Add(viewModel.View) region.Activate(viewModel.View)

The region manager service reference was obtained through dependency injection in the constructor of the module class. The preceding code first indexes into the Regions collection with the name of the region, which returns an IRegion reference. Then it calls Add on that region, passing the view reference. In this case, the code obtains the view reference through the View property exposed by the ViewModel. Finally, to be sure the view is displayed immediately, call Activate on the region, passing the view again. It is up to the region implementation to decide whether to present a view immediately when adding it; in this case, when the region is based on a ContentControl, the view is not displayed unless you call Activate.



Comment and Contribute

 

 

 

 

 


(Maximum characters: 1200). You have 1200 characters left.

 

 

Sitemap