In Part 2 of this series, I introduced the Shell and the Bootstrapper components of a Prism application. The stage is set. The crew's in place. It's time for Lights! Camera! Action! But first, we need to hire some actors.
Modules
To quote the Prism documentation itself:
A module is a logical collection of functionality and resources that is packaged in a way that can be separately developed, tested, deployed, and integrated into an application.
All modules contain a central class responsible for initialization of the module and integration of it into the application. This class implements the IModule interface, which consists of one method: Initialize(). Typically, this method and a constructor are the only methods needed for this class, but I'll share more on that in a bit.
Before we start making our modules, we should probably decide how we want Prism to know where to find them. This is where the Module Catalog comes in.
The Module Catalog
In a composite application, modules must be discovered and loaded at run time by the host application (the Shell in Prism). Prism accomplishes this through a module catalog which specifies the modules to load, when to load them and in what order. There are several choices for filling the module catalog:
- Registering modules in code
- Registering modules in XAML
- Registering modules in a configuration file
- Discovering modules in a local directory on disk
- Derive from ModuleCatalog class to create your own custom module catalog behavior
Which mechanism you use depends on the needs of your application (there's also nothing stopping you from using more than one mechanism if you are so inclined). Each module catalog class implements the IModuleCatalog interface. For this application we will use directory discovery (via the DirectoryModuleCatalog class), but a brief description of the other methods follows.
Registering Modules in Code
The ModuleCatalog class is the most basic IModuleCatalog implementation. It is used to programmatically register modules by specifying each module class type. You can also specify properties on each module such as the name, initialization mode, other module dependencies and the loading state.
To register a module, call the AddModule() method of the ModuleCatalog class. This is typically done in an override of the Bootstrapper method ConfigureModuleCatalog(). With this approach, you can use conditional logic to determine which modules should be included in your application. The modules added in code are referenced by the application rather than being loaded at runtime like other methods. However, unless you want to provide a direct reference in your application to each module’s assembly, thereby giving the Shell project knowledge of what modules exist (ideally the Shell shouldn’t know anything about what modules are out there), you will have to use reflection in your Bootstrapper method to provide the fully qualified type name and location of the assembly that contains the module.
Here's an example of how you might register a module in code (note that it assumes your Shell project has a static reference to the module project):
protected override void ConfigureModuleCatalog()
{
var myModuleType = typeof(MyModule);
Prism.Bootstrapper.ModuleCatalog.AddModule(
new ModuleInfo()
{
ModuleName = myModuleType.Name,
ModuleType = myModuleType.AssemblyQualifiedName,
DependsOn = new Collection<string>(new List<string>()
{
"MyOtherModule",
"MyServiceModule"
})
});
}
In the above code, the DependsOn property can also be set in the attributes of the module class instead of here. The attributes are also where you set the name of the module referenced by myModuleType.Name.
Registering Modules in a XAML File
Another option to declaratively define a module catalog is via a XAML file. Typically, you add the file as a resource to the Shell project. You then override the CreateModuleCatalog() method of your Bootstrapper and within that method call the CreateFromXaml() method of class Prism.Modularity.ModuleCatalog.
The XAML might look something like this:
<!-- ModulesCatalog.xaml -->
<Modularity:ModuleCatalog xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:Modularity="clr-namespace:Microsoft.Practices.Prism.Modularity;assembly=Microsoft.Practices.Prism">
<Modularity:ModuleInfoGroup InitializationMode="OnDemand">
<Modularity:ModuleInfo Ref="file://ModularityWithUnity.Desktop.MyOtherModule.dll" ModuleName="MyOtherModule" ModuleType="ModularityWithUnity.Desktop.MyOtherModule, ModularityWithUnity.Desktop.MyOtherModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<Modularity:ModuleInfo Ref="file://ModularityWithUnity.Desktop.MyServiceModule.dll" ModuleName="MyServiceModule" ModuleType="ModularityWithUnity.Desktop.MyServiceModule, ModularityWithUnity.Desktop.MyServiceModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<Modularity:ModuleInfo Ref="file://ModularityWithUnity.Desktop.MyModule.dll" ModuleName="MyModule" ModuleType="ModularityWithUnity.Desktop.MyModule, ModularityWithUnity.Desktop.MyModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Modularity:ModuleInfo.DependsOn>
<sys:String>MyOtherModule</sys:String>
<sys:String>MyServiceModule</sys:String>
</Modularity:ModuleInfo.DependsOn>
</Modularity:ModuleInfo>
</Modularity:ModuleInfoGroup>
</Modularity:ModuleCatalog>
Modules don't have to be in a ModuleInfoGroup, but it allows for setting common properties for modules and is the only way to set module dependencies. In the Bootstrapper, you'd then have the following for ConfigureModuleCatalog():
protected override IModuleCatalog CreateModuleCatalog()
{
return Prism.Modularity.ModuleCatalog.CreateFromXaml(new Uri("/PrismSample;component/ModulesCatalog.xaml", UriKind.Relative));
}
Registering Modules in a Configuration File
Similar in idea to registering in a XAML file, this method has the added benefit of not requiring the application to be recompiled if you want to add and remove modules at runtime since the file is not compiled into the application. Setting attribute startupLoaded="true" on a module will automatically load the module.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="modules" type="Microsoft.Practices.Prism.Modularity.ModulesConfigurationSection, Microsoft.Practices.Prism"/>
</configSections>
<modules>
<module assemblyFile="ModularityWithUnity.Desktop.MyOtherModule.dll" moduleType="ModularityWithUnity.Desktop.MyOtherModule, ModularityWithUnity.Desktop.MyOtherModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" moduleName="MyOtherModule" startupLoaded="false" />
<module assemblyFile="ModularityWithUnity.Desktop.MyServiceModule.dll" moduleType="ModularityWithUnity.Desktop.MyServiceModule, ModularityWithUnity.Desktop.MyServiceModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" moduleName="MyServiceModule" startupLoaded="false" />
<module assemblyFile="ModularityWithUnity.Desktop.MyModule.dll" moduleType="ModularityWithUnity.Desktop.MyModule, ModularityWithUnity.Desktop.MyModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" moduleName="MyModule" startupLoaded="false">
<dependencies>
<dependency moduleName="MyOtherModule"/>
<dependency moduleName="MyServiceModule"/>
</dependencies>
</module>
<module assemblyFile="ModularityWithUnity.Desktop.MyAutoLoadModule.dll" moduleType="ModularityWithUnity.Desktop.MyAutoLoadModule, ModularityWithUnity.Desktop.MyAutoLoadModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" moduleName="MyAutoLoadModule" startupLoaded="true" />
</modules>
</configuration>
To specify that the configuration file is the source of your module catalog, you would add the following code to the Bootstrapper:
protected override IModuleCatalog CreateModuleCatalog()
{
return new ConfigurationModuleCatalog();
}
Discovering Modules in a Local Directory on Disk
This is the method we will be using for this blog series. The module catalog scans the specified directory searching for assemblies defining each module in the application. This method requires the use of declarative attributes in the module classes to define the module names and any dependencies. (This is different from the other approaches where these attributes are defined as part of the catalog itself.) Aside from a mechanism to copy the module libraries to the catalog directory (typically accomplished by adding a post-build copy event to the module project), the only other thing needed is the following override in the Bootstrapper:
protected override IModuleCatalog CreateModuleCatalog()
{
// Use local directory as module catalog
return new DirectoryModuleCatalog { ModulePath = "Modules" };
}
If we were using MEF instead of Unity, we wouldn't override CreateModuleCatalog(). Instead we'd create a new instance of the DirectoryCatalog class and add that to the MEF AggregateCatalog as follows:
/// <summary>
/// Imperatively add type registrations to MEF's Aggregate Catalog
/// </summary>
protected override void ConfigureAggregateCatalog()
{
base.ConfigureAggregateCatalog();
// Add Shell assembly
this.AggregateCatalog.Catalogs.Add(new AssemblyCatalog(typeof(Bootstrapper).Assembly));
// Add module catalog
var moduleCatalog = new DirectoryCatalog("Modules");
this.AggregateCatalog.Catalogs.Add(moduleCatalog);
}
Create the Modules
We've defined our module catalog, so it's time to create the modules. We start by adding a folder to our solution tree called Modules. (This step isn't strictly necessary, but I find it nice for organization of the assemblies. Your mileage may vary.) Right-click on the Modules folder and select Add→New Project.... Name the class library NavigationMenu and click OK.
Delete the Class1.cs file that is created. Next we add the Prism and Unity libraries to our module. This process is identical to what we did with the Shell project. Right click on the NavigationMenu project and select Manage NuGet Packages.... Click on the Browse tab and type prism.unity in the search box. Select the Prism.Unity package and install it.
Now is as good of a time as any to also add a reference to our Infrastructure assembly. We will define all our module names as constants in a static class as part of Infrastructure. After adding a reference to the Infrastructure project in our module, right click on the Constants folder in the project and select Add→New From Template→Class. In the popup box type ModuleNames and click OK. Add a constant for the Navigation Menu module name to this class as follows:
namespace Infrastructure.Constants
{
public static class ModuleNames
{
public const string NAVIGATION_MENU = "NavigationMenu";
}
}
The next step to is to create the module initialization file, which defines the module attributes and registers the interfaces and classes the module implements. Right click on the NavigationMenuModule project and select Add→New From Template→Class. In the popup box type NavigationMenuModule and click OK. This new class needs to implement the IModule interface, which consists of one method called Initialize(). The class also needs a declarative ModuleAttribute applied that defines at least the ModuleName property. In this case we set the property to the constant we defined above as follows:
using Infrastructure.Constants;
using Prism.Modularity;
namespace NavigationMenu
{
/// <summary>
/// Register components of module with Unity/Prism
/// </summary>
[Module(ModuleName = ModuleNames.NAVIGATION_MENU)]
public class NavigationMenuModule : IModule
{
public void Initialize()
{
}
}
}
We still have to copy the module library to our catalog folder. To this we open the properties of our NavigationMenu project and add the following post-build event:
robocopy "$(TargetDir)\" "$(SolutionDir)PrismSample\$(OutDir)Modules" NavigationMenu.dll
if %errorlevel% geq 8 exit 1
exit 0
(Note: Instead of robocopy (i.e. Robust Copy) some people might use xcopy here. The problem with xcopy is that Microsoft officially deprecated it a while ago. Additionally, robocopy has nicer error handling. I have found, however, that sometimes robocopy seems to hang for a while when building the application in Visual Studio, particularly when many modules are built. I've yet to figure out why, but it might be that robocopy is slow to release the directory after it finishes copying, so when the next module tries to copy to the directory it can't get access. Occasionally, robocopy will even time out and the build will fail. Building again usually cleans up the issue.)
The final step to getting our module to load is to ensure the solution builds this module before it builds the shell. Right-click on the PrismSample solution in Solution Explorer and select Project Dependencies.... Select the PrismSample project under the Projects: drop-down and you should see that Infrastructure is already checked. Check NavigationMenu as well. If you look at the Build Order tab you should see Infrastructure listed first in the order and Prism Sample listed last.
Following a similar process to above that we used with NavigationMenu, we can add the TopRegion, MiddleRegion, and BottomRegion modules as well. (As mentioned in the previous part of this series in regards to the shell regions, a real application would have more descriptive names for what the other three modules actually do.) Our application will run, but since we haven't implemented any actual functionality in our modules, it does nothing other than display a logo at this point. For the final section of this part of the series, I'll create a simple menu view and show how to register it with a region for display.
View Composition (Discovery)
View composition is the construction of a view. There are two ways to create and display a view in a region:
- Automatically through discovery
- Programmatically through injection
Discovery sets up a relationship in the RegionViewRegistry between a region's name and a type of view. When a region is created, all view types associated with the region are automatically instantiated and their corresponding views loaded. With this method there is no explicit control over when views are loaded and displayed.
Injection involves obtaining a reference to a region and then programmatically injecting views into it. This gives you full control over when views are loaded, removed, and displayed. However, you cannot add a view to a region that has not been created yet.
For this part of the blog series, we concern ourselves only with discovery. We'll come back to injection in a later part of this series when we introduce the View Navigation API.
We start by creating the view for our menu by adding a new WPF User Control to our NavigationMenu project:
(Note: After creating the user control, we'll likely need to add a reference to System.Xaml to our project.)
At this point it makes sense to create a Resource Dictionary to hold the control styles common to the various views in our application. We'll add a SharedResources folder to the Infrastructure project and to that folder we add a new resource dictionary. Since our Infrastructure project is a Class Library, however, we can't add the file through the convenience of the Add New Item dialog, so we'll have to do it manually. (Or if you use Resharper like I do, you can select the "More..." menu from the Add New From Template menu and it will give you the option to add one there.) We'll name the resource dictionary ResourceDictionary.xaml, and once we add it, we'll also add a reference to the .NET PresentationFramework library to our Infrastructure project so that the resource dictionary is supported. For now our resource dictionary is pretty straightforward and looks like this:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<!--Fonts-->
<system:Double x:Key="NormalFontSize">26</system:Double>
<FontFamily x:Key="NormalFont">Century Gothic</FontFamily>
<!--Colors-->
<Color x:Key="NormalFontColor">#FF5F5F5F</Color>
<!--Brushes-->
<SolidColorBrush x:Key="NormalForegroundBrush" Color="{StaticResource NormalFontColor}" />
<!--TextBlocks-->
<Style x:Key="NormalTextBlockStyle" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{StaticResource NormalFont}" />
<Setter Property="Foreground" Value="{StaticResource NormalForegroundBrush}" />
<Setter Property="FontSize" Value="{StaticResource NormalFontSize}" />
</Style>
<!--Controls-->
<Style x:Key="BaseControlStyle" TargetType="Control">
<Setter Property="FontFamily" Value="{StaticResource NormalFont}" />
<Setter Property="Foreground" Value="{StaticResource NormalForegroundBrush}" />
<Setter Property="FontSize" Value="{StaticResource NormalFontSize}" />
</Style>
</ResourceDictionary>
In order for this to build, we'll need to add references to the PresentationCore, WindowsBase and System.Xaml libraries to our Infrastructure project.
And while we're add it, this would be a good point to add localizable strings to that project as well (though, this application will never need to worry about it). We'll add a new folder called Strings as a subfolder to our SharedResources folder and to that new folder add a new Resources File with a Public access modifier we'll call Text.resx.
Going back to our NavigationMenu.xaml file, we will implement a fairly simple menu as follows:
<UserControl x:Class="NavigationMenu.NavigationMenu"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:str="clr-namespace:Infrastructure.SharedResources.Strings;assembly=Infrastructure"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="800" d:DesignWidth="300">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Infrastructure;component/SharedResources/ResourceDictionary.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<StackPanel Background="White">
<StackPanel.Resources>
<Style TargetType="Button" BasedOn="{StaticResource BaseControlStyle}">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Padding="10" Background="{TemplateBinding Background}">
<ContentPresenter VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="#2D82B9" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Foreground" Value="{StaticResource NormalForegroundBrush}" />
<Setter Property="Background" Value="LightGray" />
</Trigger>
</Style.Triggers>
</Style>
</StackPanel.Resources>
<Button Content="{x:Static str:Text.About}" />
<Button Content="{x:Static str:Text.CreateError}" />
<Button Content="{x:Static str:Text.Exit}" />
</StackPanel>
</Grid>
</UserControl>
Finally, in NavigationMenuModule.cs we will register our module via the Module declarative attribute and then in the Initialize() method of the IModule interface we register this view with the region we've defined for it:
using Infrastructure.Constants;
using Prism.Modularity;
using Prism.Regions;
namespace NavigationMenu
{
/// <summary>
/// Register components of module with Unity/Prism
/// </summary>
[Module(ModuleName = ModuleNames.NAVIGATION_MENU)]
public class NavigationMenuModule : IModule
{
private readonly IRegionManager _regionManager;
public NavigationMenuModule(IRegionManager regionManager)
{
this._regionManager = regionManager;
}
public void Initialize()
{
this._regionManager.RegisterViewWithRegion(RegionNames.MENU_REGION, typeof(NavigationMenu));
}
}
}
For MEF, registration is a bit different. We first need to add the [Export] attribute to our NavigationMenu.xaml.cs file as follows:
using System.ComponentModel.Composition;
using System.Windows.Controls;
namespace NavigationMenu
{
/// <summary>
/// Interaction logic for NavigationMenu.xaml
/// </summary>
[Export]
public partial class NavigationMenu : UserControl
{
public NavigationMenu()
{
this.InitializeComponent();
}
}
}
And then in our module file we register the module using the Module attribute as we did in Unity, but we also have to add a ModuleExport attribute for the module itself, and declare the constructor as an ImportingConstructor so that the region manager can be set through dependency injection:
using System.ComponentModel.Composition;
using Infrastructure.Constants;
using Prism.Mef.Modularity;
using Prism.Modularity;
using Prism.Regions;
namespace NavigationMenu
{
/// <summary>
/// Register components of module with MEF/Prism
/// </summary>
[Module(ModuleName = ModuleNames.NAVIGATION_MENU)]
[ModuleExport(typeof(NavigationMenuModule), InitializationMode = InitializationMode.WhenAvailable)]
public class NavigationMenuModule : IModule
{
private readonly IRegionManager _regionManager;
[ImportingConstructor]
public NavigationMenuModule(IRegionManager regionManager)
{
this._regionManager = regionManager;
}
public void Initialize()
{
this._regionManager.RegisterViewWithRegion(RegionNames.MENU_REGION, typeof(NavigationMenu));
}
}
}
If we build the application, we now see our menu has populated the region on the left-hand side. (The other three modules have also been added with place-holder views.) Of course, it doesn't do anything at this point, but we will shortly rectify that in Part 4: Intermodule Communication (coming soon).
The Unity-based solution files and source code for this part can be downloaded by clicking here.
The MEF-based solution files and source code for this part can be downloaded by clicking here.
A Properly Pleasing Prism Primer - The Parts:
Part 1: An Introduction
Part 2: The Shell and Bootstrapper
Part 3: Modules
Part 4: Intermodule Communication (Coming soon)
Learn more about DMC's .NET application development services.