Tuesday, February 5, 2013

Prism, WPF and Unity based modular application step by step



Starting a modular application development using Prism, WPF and Unity container.

Step 1

·         In Visual Studio 2010, start a new WPF project.
·         Remove MainWindow.xaml file that was automatically created.
·         Remove StartupUri="MainWindow.xaml" from App.xaml file.
·         Add the following references
o    Microsoft.Practices.Prism
o   Microsoft.Practices.Prism.Unity
o   Microsoft.Practices.Prism.UnityExtensions
o   Microsoft.Practices.Prism.ServiceLocation
o   Microsoft.Practices.Prism.Interactivity

·         Create a class called Bootstrapper.cs and derive this from UnityBootstrapper class.
·         Create  the main shell window by adding a new wpf window called PrismAppShell.xaml
·         Implement the abstract method CreateShell in Bootstrapper class.
·         Override two methods in Bootstrapper class
o    InitializeModules
o    ConfigureModuleCatalog
·        
      Inside App.xaml.cs within OnStartup method instantiate Bootstratper object and call its Run()

Therefore the files are going to look like this

App.xaml

<Application x:Class="Prism.ModuleExmaple.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            >
    <Application.Resources>
        
    </Application.Resources>
</Application>


App.xaml.cs

public partial class App : Application
       {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);

            BootStrapper bootstrapper = new BootStrapper();
            bootstrapper.Run();
        }
       }



Bootstrapper.cs

public class BootStrapper : UnityBootstrapper
    {
        protected override System.Windows.DependencyObject CreateShell()
        {
            return this.Container.Resolve<PrismAppShell>();
        }

        protected override void InitializeModules()
        {
            base.InitializeModules();
            App.Current.MainWindow = (PrismAppShell)this.Shell;
            App.Current.MainWindow.Show();
        }

        protected override void ConfigureModuleCatalog()
        {
            base.ConfigureModuleCatalog();

            this.ModuleCatalog.AddModule(null); // TODO - placeholder
        }
    }




PrismAppShell.xaml


<Window x:Class="Prism.ModuleExmaple.PrismAppShell"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="PrismAppShell" Height="300" Width="300">
    <Grid>
       
    </Grid>
</Window>



Now one with run this application from visual studio the empty shell window will come up like below.



Step 2

Now let us divide the main shell xaml  into regions and mark them with regional names.



<Window x:Class="Prism.ModuleExmaple.PrismAppShell"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:prism="http://www.codeplex.com/prism"
        Title="PrismAppShell" Height="900" Width="1200">
    <Grid x:Name="LayoutRoot">
        <DockPanel LastChildFill="True" HorizontalAlignment="Stretch" Name="dockPanel1" VerticalAlignment="Stretch">

            <StackPanel Orientation="Horizontal" DockPanel.Dock="Top" Background="#FFCCD4F8" Height="100">
                <ContentControl prism:RegionManager.RegionName="RibbonRegion"></ContentControl>
            </StackPanel>
            <StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom" Background="#FFD9E1EF" Height="100">
                <ContentControl prism:RegionManager.RegionName="StatusRegion"></ContentControl>
            </StackPanel>
            <ScrollViewer>
                <StackPanel Orientation="Vertical" DockPanel.Dock="Left" Background="#FF50576F" Width="200">
                    <ContentControl prism:RegionManager.RegionName="TreeRegion"></ContentControl>
                </StackPanel>
            </ScrollViewer>
            <StackPanel Orientation="Vertical" DockPanel.Dock="Right" Background="#FF677BA7" Width="100">
                <ContentControl prism:RegionManager.RegionName="AlertRegion"></ContentControl>
            </StackPanel>
            <ScrollViewer HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
                <StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="#FFC0DBF2">
                    <ContentControl prism:RegionManager.RegionName="BlotterRegion" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"></ContentControl>
                </StackPanel>
            </ScrollViewer>

        </DockPanel>
       
    </Grid>
</Window>







Step 3

In step 3. let us create few individual modules such and load them on the regions on the shell as we defined before. Also in the shell xaml on the top we used a ribbon control and defined the RibbonRegion there, a TreeView control on the left for TreeRegion, a TabControl in the middle for BlotterRegion. Bottom and  right (StatusRegion and AlertRegion) we kept as ContentControl still.









<Window x:Class="PrismApp.Shell.PrismAppShell"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:prism="http://www.codeplex.com/prism"
        Title="PrismAppShell" Height="900" Width="1200"
        xmlns:my="http://schemas.microsoft.com/winfx/2006/xaml/presentation/ribbon"
        >
   
    <Window.Resources>
       
        <DataTemplate x:Key="crap">
            <StackPanel Width="100" Height="18"></StackPanel>
        </DataTemplate>
       
        <Style x:Key="TabItemStyleKey" TargetType="{x:Type TabItem}">
            <Setter Property="Header" Value="{Binding Path=DataContext.ViewTile}"></Setter>
            <Setter Property="HeaderTemplate" Value="{Binding Source={StaticResource crap}}"></Setter>
        </Style>

        <Style x:Key="TreeItemStyleKey" TargetType="{x:Type TreeViewItem}">
            <Setter Property="Height" Value="Auto"></Setter>
            <Setter Property="HorizontalAlignment" Value="Left"/>
            <Setter Property="VerticalAlignment" Value="Top" />
            <Setter Property="BorderThickness" Value="0" />
        </Style>
       
    </Window.Resources>
   
    <Grid x:Name="LayoutRoot">
        <DockPanel LastChildFill="True" HorizontalAlignment="Stretch" Name="dockPanel1"
                   VerticalAlignment="Stretch">

            <my:Ribbon prism:RegionManager.RegionName="RibbonRegion" DockPanel.Dock="Top"
                       HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Name="ribbon1" Height="150"/>

            <StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom" Background="#FFD9E1EF" Height="100">
                <ContentControl prism:RegionManager.RegionName="StatusRegion"></ContentControl>
            </StackPanel>
           
            <ScrollViewer>
                <StackPanel Orientation="Vertical" DockPanel.Dock="Left" MinWidth="150" MaxWidth="200">
                    <TreeView ItemContainerStyle="{StaticResource TreeItemStyleKey}"
                              prism:RegionManager.RegionName="TreeRegion"
                              Name="treeView1"
                              HorizontalAlignment="Stretch"
                              VerticalAlignment="Stretch"
                              BorderThickness="0"
                              />
                </StackPanel>
            </ScrollViewer>
           
            <StackPanel Orientation="Vertical" DockPanel.Dock="Right" Background="#FF677BA7" Width="100">
                <ContentControl prism:RegionManager.RegionName="AlertRegion"></ContentControl>
            </StackPanel>

           
            <ScrollViewer HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
                <StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="#FFC0DBF2">
                    <TabControl ItemContainerStyle="{StaticResource TabItemStyleKey}"
                                prism:RegionManager.RegionName="BlotterRegion" Name="tabControl1"
                                HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Width="Auto">
                    </TabControl>
                </StackPanel>
            </ScrollViewer>
        </DockPanel>       
    </Grid>
</Window>



Cash blotter module registers view like this.

public class CashBlotterModule : IModule
    {
        private readonly IRegionViewRegistry regionViewRegistry = null;

        public CashBlotterModule(IRegionViewRegistry regionViewRegistry)
        {
            this.regionViewRegistry = regionViewRegistry;
        }

        public void Initialize()
        {
            this.regionViewRegistry.RegisterViewWithRegion("BlotterRegion", typeof(CashBlotterView));
            this.regionViewRegistry.RegisterViewWithRegion("TreeRegion", typeof(CashItemsTreeView));
        }
    }


And bootstrapper loads module like this. (IN next section this will be worked on further to load using container and decouple.

public class BootStrapper : UnityBootstrapper
    {
        protected override System.Windows.DependencyObject CreateShell()
        {
            return this.Container.Resolve<PrismAppShell>();
        }

        protected override void InitializeModules()
        {
            base.InitializeModules();
            App.Current.MainWindow = (PrismAppShell)this.Shell;
            App.Current.MainWindow.Show();
        }

        protected override void ConfigureModuleCatalog()
        {
            base.ConfigureModuleCatalog();
            ModuleCatalog moduleCatalog = (ModuleCatalog)this.ModuleCatalog;
          moduleCatalog.AddModule(typeof(PrismApp.Module.Cash.Blotter.CashBlotterModule)); // Seems like as soon as you add a new module, internally seletor module adopter (since blotter region is on tab control), automatically adds a tab item)
            moduleCatalog.AddModule(typeof(PrismApp.Module.Deriv.Blotter.DerivBlotterModule));
            //moduleCatalog.AddModule("CashBlotterModule", "PrismApp.Module.Cash.Blotter.CashBlotterModule");
        }
    }

In above code, inside ConfigureModuleCatalog the module loading will be upgraded in the following section.


When we run now, the skeleton application comes up like this




Step 4

In step 4, we have

  • Replaced the wpf datagrid with devexpress grid control, wpf toolbox ribbon with devexpress ribbon control.
  • View Injection - done view injection from cash blotter module and ticket module and as well from derivatives blotter and ticket module.
  • Injected views in desired prism regions on the shell, i.e. on BlotterRegion, RibbonRegion, TreeRegions.
  • Model - connected to database for fake trade data.
  • ViewModel - created a number of view model classes in order for the views to bind to trades data.


After all the above steps in step 4, our skeleton app looks like the following.

12 comments:

  1. How far have you gone in this implementation? what mechanism you are using to push values to all the connected blotters from the server

    ReplyDelete
  2. Hi Milind,

    You should be able to have full fledge production quality applications based on the above structure. However for this blog I just did with play data.

    For a real system, you can use any number of data sources, it does not matter. You can use Rx (Reactive Extension) if you desire push based data, or in memory chached data (with or without commercial in-memory product) and then register event handlers from blotter-view-models to get updates for blotter. View models can listen to message queues, or retrive data from databases or have LINQ querries, object data sources, anything you would like.

    For simple straight forward way, you can directly get from database (for this play application that is what I am doing, I am getting dummy data from my local sql server instance).

    ReplyDelete
    Replies
    1. Thanks, Can you please share the sample code for this app?

      Delete
  3. I was hoping that your blog article would be a good intro to Prism, but in VS2012 and Prism 4.1 I am getting a syntax error on your CreateShell method. The return line underscores the Resolve() method and states that it cannot be used with type arguments. Any ideas? I will continue on with the project for background purposes but would really like to run the program.

    ReplyDelete
  4. Hi Gerald, could you please upload the source code? I am new to PRISM....
    Thanks!

    ReplyDelete
  5. I'm fairly knowledgeable with WPF and MVVM. I followed the above steps (up to step 3) but the solution won't compile. Can you provide the source code?

    ReplyDelete
  6. Hi,
    What about all the code source please ? Could you send it to me all the source code please ?

    ReplyDelete
  7. Thanks, Can you please share the sample code for this app?

    ReplyDelete
  8. Great article. I am interested in module navigation and dynamically ribbon population based on modules loaded. Any implementation hints?

    Thanks!

    ReplyDelete
  9. How do you connect your viewmodel to view in this scenario. I have a similar structured project where I have a main project with a xaml file that contains contentcontrol region and another project as a module where I define a UserControl. I load my modules using a app.config file rather that coding in Bootstrapper. I am able to successfuly display usercontrol in the contentcontrol using RegionManager.RegisterViewToRegion(...) and I do this in Initialize method in a class that implements IModule interface. No the trouble I have is how do i tie or register this user control to a viewmodel that is in same module as the user control. I tried few different ways but unsuccessful. I am using PRISM 6.3. Any thoughts or suggesstion is greatly appreciated.

    ReplyDelete
  10. Nicely explained article by the way...

    ReplyDelete