Windows Presentation Foundation (WPF) was introduced
with the .NET framework 3.0 and is comprised of a series of classes and
tools that leverage modern advances in graphics and UI technology in
support of establishing a framework for highly composable presentation
layer design. This framework is specifically based on a partitioning of
UI appearance and behavior, essentially allowing a separation of
concerns to be applied to logic related to user experience.
Silverlight is a
Microsoft development platform (and also the name of a popular browser
plug-in) specific to the creation of Web-based, interactive
user-interfaces.
Note
WPF provides support for
the Extensible Application Markup Language (XAML) industry standard. To
view the XAML specification, visit www.soaspecs.com. Also, to learn more about Silverlight, visit the official Microsoft Silverlight site at www.silverlight.net.
The Prism Library (also known as the Composite Application Library for WPF and Silverlight)
was developed by Microsoft in support of building service compositions
with WPF and Silverlight. Prism can be considered a manifestation of the
Service Composability principle and the Functional Decomposition pattern, as they apply to presentation layer logic. (It further relies
on additional UI-centric design patterns, as explained shortly.)
Prism enables the
development of client applications that are intrinsically loosely
coupled, with a clear separation of user interface artifacts (such as
views and toolbars), business logic artifacts (such as service agents
and entities), and shared services.
User interface composition
is achieved by providing for a common user experience that composes one
or more user experience capabilities contributed from various back-end
services and resources. This results in the appearance of a seamless,
harmonized user experience for the end-user, irrespective of the number
and diversity of moving parts on the back-end.
There are five primary parts to the Prism Library:
shell
views
regions
modules
shared services
Let’s take a look at each separately.
Shell
The shell
is the top-level or primary window of a body of client-side logic. The
shell may be composed of multiple windows, but usually it is just a
single main window that contains multiple views (explained shortly), as
shown in Figure 1.
A system itself may have more than one shell or top-level window, and
each top-level window acts as the shell for the content that it composes
and contains.
Specifically, the shell defines:
the overall appearance of the client UI
various top-level UI elements (such as the main menu and toolbar)
styles and borders that are present and visible in the shell layout
styles, templates, and themes that are applied to the views that are plugged into the shell
The following code shows an implementation of a very simple shell:
Example 1.
public partial class SimpleShell : Window, IShellView { public SimpleShell() { InitializeComponent(); } public void ShowView() { this.Show(); } }
|
...and the IShellView interface is defined as follows :
Example 2.
public interface IShellView { void ShowView(); }
|
In this simple example, the implementation of ShowView calls Show on the shell class, which displays the main window. ShowView is called as part of the initialization of the client application.
Views
Views
are constituent units for UI composition. User controls, data
templates, and custom controls are examples of Prism views. Each view
basically represents a portion of the user interface. Collectively, the
views are composed and rendered in the shell’s windows. The part of the
UI captured by a view is naturally decoupled from the rest of the client
application logic. You can think of a view as a module for UI rendering
purposes.
The easiest and most common
way to define a view is to define a user control. The following markup
shows an example WPF user control:
Example 3.
<UserControl x:Class="WpfApplication1.SimpleView" xmlns="http://schemas.microsoft.com/ winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Height="25" Width="100"> <Label> A Simple View </Label> </UserControl>
|
Sometimes a view that is being
used by multiple services can become complicated. In this case, it may
make sense to break up the view into several child views and have the
parent view construct itself using these child views. It may do this
statically at design-time, or it may support the adding of child views
at runtime. A view that is not fully defined in a single view class is
referred to as a composite view.
View Discovery versus View Injection
Views
can be created and displayed automatically through the view discovery
approach , and also
programmatically, through an approach called view injection.
With the view discovery approach, a relationship is created in the RegionViewRegistry between a region’s name and the type of a view. When a region is created, the region is designed to walk through all of the ViewTypes
associated with the region and automatically instantiate and load them.
With view discovery, we do not have fine-grained control over when the
regions’ corresponding views are loaded and displayed.
When following the view
injection approach, the application programmatically obtains a reference
to a region and injects or adds a view to it. Typically, this is done
when a module initializes and also as the result of a user action. With
view injection, we have more control over when views are loaded and
displayed; we also have the ability to remove views from the region.
Service compositions
necessitate the communication and the sharing of context and other
associated information across the views. Prism provides multiple
techniques for communicating between views, and the region manager
enables RegionContext as one of these approaches. RegionContext is particularly useful when there is a need to share context between a parent view and child views that are hosted in a region.
Regions
In composite applications,
multiple views may be displayed at runtime in specific locations within
the application’s user interface. To achieve this, you need to define
the locations where the views will appear and how the views will be
created and displayed in those locations.
You can determine where views will appear by defining a layout with named locations, known as regions.
Regions act as placeholders within which one or more views are
displayed at runtime. You can locate and add content to regions in the
layout without exact knowledge of how or where the region is visually
displayed. This allows the layout to change without affecting the
modules that add the content to the layout (as shown in Figure 2).
Essentially,
regions are used to decouple the view from the location in the shell
where the view will be displayed. This allows the layout and the
appearance of the application to evolve independently of the views. More
specifically, a region is defined by assigning a region name to a WPF
or Silverlight control. At runtime, views are added to the named region
control, which then displays the view (or views) according to the layout
strategy that they implement. For example, a tab control region will
lay out its child views in a tabbed arrangement.
The following example demonstrates the use of regions in a shell:
Example 4.
<ItemsControl x:Name="MainToolbar" Regions:RegionManager.RegionName= "MainToolBarRegion" ...> ... <Controls:AnimatedTabControl Regions:RegionManager.RegionName= "PrimaryRegion" .../> ... <Controls:StockDataControl Regions:RegionManager.RegionName= "StockDataRegion"> ... </ItemsControl>
|
Note that regions can be
accessed by their region name and support the addition or removal of
views. Views can be created and displayed in regions either
programmatically or automatically.
Modules
Up
until now we have described the addition of content to the regions in
an abstract manner. The actionable part that you use to actually specify
the views that will appear in a region is the module.
Modules are the logical units of the client application that enable:
Modules are commonly used to
represent service agents or proxies for back-end services. This further
enables the invocation of various line-of-business applications and
legacy systems and resources. Modules may also be used to represent
utility services (such as authentication and logging).
There are different ways of
creating and packaging modules, but the most common means is to create a
single assembly per module. This assembly contains all of the views,
services, and other classes needed by the module. It also must contain a
class that derives from IModule, as follows:
Example 5.
public interface IModule { void Initialize(); }
|
Modules go through three lifecycle phases:
1. | Define / Discover Modules
In the first phase, information about the modules is added to a catalog
or registry. Modules may be defined in regular code or with XAML and
they can also be defined by reading in module information from a
configuration file. Modules can further be discovered and defined by
loading in all annotated modules (assemblies) in a designated directory.
|
2. | Load Modules
Next, modules are loaded from disk into memory in order for
initialization purposes. If the assembly is not present on disk, it
might have to be retrieved first; for example, by downloading assemblies
from the Web using Silverlight XAP files.
|
3. | Initialize Modules
Finally, the module is initialized. An instance of the module is created and its Initialize()
method is invoked. Initialization is the phase where views are
registered with regions and where the module is integrated with the
application. Integration may involve a number of activities, including
them being integrated with the application’s navigation structure (such
as responding to a user click on a menu item to display a certain view
and subscribing to application-specific events).
|
Shared Services
Prism promotes extensibility by allowing you to add or replace capabilities, as well as shared services.
Shared services establish the intrinsic loose coupling across and
between modules and the shell, which are enabled by the use of
dependency injection containers (Figure 3).
A dependency injection
container (also referred to as a DI container) serves to reduce the
coupling between objects via the instantiation of instances of classes
and the management of their lifetime. It accomplishes this using
external metadata or configuration information.
During the creation
of an object, the container injects any dependencies that the object has
requested into it. For example, modules often get the container
injected so they can register their views and services with that
container. In addition to the container, Prism provides a number of
shared services, such as those for managing module lifecycles (as per
the three phases described in the previous Modules section) and managing regions.