Thursday, June 30, 2016

Modular application on WPF + Caliburn.Micro + Castle.Windsor

First I want to specify that this article refers to a modular application. So, we assume a modular application is an application that consists of so-called shell and a set of plug-ins. Between them there is no direct relationship, only through contracts. This allows to make changes independently of each components composition change them, etc. I think, all without me well aware of the benefits of a modular architecture.

image

Perhaps the most well-known framework for creating WPF applications with such architecture is a Prism. In this article I will not make comparisons, because I have no experience of using the Prism. After reading the tutorial, the Prism with all its regions, mefom and other artifacts, seemed much complexity;. If the reader who knows the Prism, rightly tells me my wrong and advantages of the framework - I will be grateful.




In this article, the development of a simple modular application will be examined with the use of these tools.

Caliburn.Micro

Caliburn.Micro - This framework greatly simplifies the description of the View and ViewModel. In fact, he creates bayndingi based naming convention, thus saving the developer from writing them by hand and making the code less and cleaner. Here are a couple of examples from their website:

<ListBox x: Name = "Products" />

public BindableCollection <ProductViewModel> Products
{
    get; private set;
}
public ProductViewModel SelectedProduct
{
    get {return _selectedProduct; }
    set
    {
        _selectedProduct = value;
        NotifyOfPropertyChange (() => SelectedProduct);
    }
}

Here in XAML, we do not specify any ItemSource, no SelectedItem.

<StackPanel>
    <TextBox x: Name = "Username" />
    <PasswordBox x: Name = "Password" />
    <Button x: Name = "Login" Content = "Log in" />
</ StackPanel>
public bool CanLogin (string username, string password)
{
    return String.IsNullOrEmpty (username) && String.IsNullOrEmpty (password)!!;
}
public string Login (string username, string password)
{
    ...
}

No Command and CommandParameter.

Agreement can be overridden when absolutely necessary.
Of course, Caliburn.Micro whatnot can. Something we look further, the rest can be found in the documentation.

Castle.Windsor

Castle.Windsor - this is one of the most famous and the most functional DI-containers for .net (it is assumed that the reader knows about DI and IoC). Yes, Caliburn.Micro, as in many other frameworks, has its own DI-container - SimpleContainer, and for further example of its capabilities would be quite enough. But for more complex tasks, he can not come, so I'll show you how to use the custom container Castle.Windsor example.

A task

As an example, I propose to consider the process of creating a simple modular applications. The main part of it - the shell - will be of a window on the left side which will ListBox-menu. the corresponding form will be displayed when you select a menu item on the right side. The menu is filled with modules that are loaded or during operation. Modules may be downloaded as a shell at start and during operation (for example, any module may load the other modules as required).

contracts

All contracts will be located in the assembly Contracts, which must refer to the shell and modules. On the basis of this problem, we write the contract of our shell.

public interface IShell
{
     IList <ShellMenuItem> MenuItems {get; }
     IModule LoadModule (Assembly assembly);
}
 public class ShellMenuItem
 {
     public string Caption {get; set; }
     public object ScreenViewModel {get; set; }
 }

I think everything is clear here. Shell allows modules to operate the menu, as well as load modules during operation. menu item contains a display name and ViewModel, the type of which can be absolutely any. When you select a menu item to the right of View will display the window corresponding to this ViewModel. How to determine what is the appropriate View? This will take care Caliburn.Micro. This approach is called ViewModel-first, because in the code we operate twist-models, and creating twist fades into the background and is given at the mercy of the framework. Details - on.

Contract module looks quite simple.

public interface IModule
 {
     void Init ();
 }

Init () method calls the party initiating the module is loaded.

It is important to note that if you build a project signed and usually is the case in major projects, it is necessary to be sure that the shell and modules used assembly contracts to one version.

We begin at the Shell

type WPF Application Project Create. Next we need to connect to the project and Caliburn.Micro Castle.WIndsor. The easiest thing to do to make it through NuGet.

PM> Install-Package Caliburn.Micro -Version 2.0.2
PM> Install-Package Castle.Windsor


But you can download the assembly, or assemble itself. Now create two folders in the project: Views and ViewModels. In ViewModels folder create ShellViewModel class; inherit it from PropertyChangedBase of Caliburn.Micro, to not implement INotifyPropertyChanged. It will twist the shell model of the main window.

class ShellViewModel: PropertyChangedBase
    {
        public ShellViewModel ()
        {
            MenuItems = new ObservableCollection <ShellMenuItem> ();
        }
        public ObservableCollection <ShellMenuItem> MenuItems {get; private set; }
        private ShellMenuItem _selectedMenuItem;
        public ShellMenuItem SelectedMenuItem
        {
            get {return _selectedMenuItem; }
            set
            {
                if (_selectedMenuItem == value)
                    return;
                _selectedMenuItem = value;
                NotifyOfPropertyChange (() => SelectedMenuItem);
                NotifyOfPropertyChange (() => CurrentView);
            }
        }
        public object CurrentView
        {
            get {return _selectedMenuItem == null? null: _selectedMenuItem.ScreenViewModel; }
        }
    }

Of course the main window MainWindow copy in View and rename in ShellView. Breakdown not forget to rename the file not only, but also the class together neymspeysom. Those. instead of a class should be Shell.MainWindows Shell.Views.ShellView. It is important. Otherwise Caliburn.Micro can not determine that this twist twist corresponds to a previously created model. As mentioned earlier, Caliburn.Micro based on naming conventions. In this case, the class name twist model removed the word «Model» and get the class name of the corresponding twist (Shell.ViewModels.ShellViewModel - Shell.Views.ShellView). In the role of View can serve Windows, UserControl, Page. The modules we will use the UserControl.
main window XAML markup-will look like this:

<Window x: Class = "Shell.Views.ShellView"
        xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns: x = "http://schemas.microsoft.com/winfx/2006/xaml"
        Title = "MainWindow" Height = "350" Width = "525">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width = "200" />
            <ColumnDefinition Width = "*" />
        </Grid.ColumnDefinitions>
     
        <ListBox x: Name = "MenuItems" DisplayMemberPath = "Caption" Grid.Column = "0" />
     
        <ContentControl x: Name = "CurrentView" Grid.Column = "1" />
    </ Grid>
</ Window>

Run Caliburn.Micro

To do this, first create a class Bootstraper with minimum content:

public class ShellBootstrapper: BootstrapperBase
    {
        public ShellBootstrapper ()
        {
            Initialize ();
        }
        protected override void OnStartup (object sender, StartupEventArgs e)
        {
            DisplayRootViewFor <ShellViewModel> ();
        }
    }

It must inherit from BootstrapperBase. OnStartup method is called when the program starts. DisplayRootViewFor () by default creates an instance of the class twist model credit default constructor is looking for an appropriate twist on the algorithm described above, and displays it.

For this to work, you need to edit an entry point into the application - App.xaml.

<Application x: Class = "Shell.App"
             xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns: x = "http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns: shell = "clr-namespace: Shell">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary>
                    <Shell: ShellBootstrapper x: Key = "bootstrapper" />
                </ ResourceDictionary>
            </ResourceDictionary.MergedDictionaries>
        </ ResourceDictionary>
    </Application.Resources>
</ Application>

We have removed StartupUri (farmed out butstrapperu) and added to the resources of our butstrapper. Such levels - not just as another project does not gather.

Now when you run the application will be created butstrapper, called OnStartup and display the main window of the application that is bound to twist model.

Pay attention to the creation of twist model. It is created by the designer by default. And if she does not have this? If it has a dependence on other entities, or other entities depend on it? I am getting to that, it was time to put to use DI-container Castle.Windsor.

Run Castle.Windsor

Create ShellInstaller class.

class ShellInstaller: IWindsorInstaller
    {
        public void Install (IWindsorContainer container, IConfigurationStore store)
        {
            container
                .Register (Component.For <IWindsorContainer> (). Instance (container))
                .Register (Component.For <ShellViewModel> () /*.LifeStyle.Singleton*/);
        }
    }

In it we will record all our components in code using fluent-syntax. There is an opportunity to do it through xml, see. The online documentation. As long as we have one component - twist model of the main window. We register it as a singleton (you can not explicitly specify it as the default LifeStyle). Also register the container itself, to be able to contact him. Looking ahead - we need it when loading modules.

Then we make changes to our butsrapper:

public class ShellBootstrapper: BootstrapperBase
    {
        private readonly IWindsorContainer _container = new WindsorContain-er ();
        public ShellBootstrapper ()
        {
            Initialize ();
        }
        protected override void OnStartup (object sender, StartupEventArgs e)
        {
            DisplayRootViewFor <ShellViewModel> ();
        }
        protected override void Configure ()
        {
            _container.Install (new ShellInstaller ());
        }
        protected override object GetInstance (Type service, string key)
        {
            return string.IsNullOrWhiteSpace (key)
                ? _container.Kernel.HasComponent (service)
                    ? _container.Resolve (service)
                    : Base.GetInstance (service, key)
                : _container.Kernel.HasComponent (Key)
                    ? _container.Resolve (key, service)
                    : Base.GetInstance (service, key);
        }
    }

Create a container. The overridden method Configure use our installer. Override GetInstance method. Its base implementation uses the default constructor to create the object. We will try to get an object from the container.

Interaction with modules

The first thing we need to learn how to load modules. And for this purpose, let us define what it represents module?

The module (in this case) - it is the assembly that contains a set of classes that implement the required functionality. One of these classes must implement the contract IModule. Furthermore, like the shell, the module must have installer registering components (classes) in the DI-unit container.

Now we proceed to the implementation of the bootloader. Download will be called at the start of the shell, and can be caused in the process, so we create a separate class.

class ModuleLoader
    {
        private readonly IWindsorContainer _mainContainer;
        public ModuleLoader (IWindsorContainer mainContainer)
        {
            _mainContainer = mainContainer;
        }
        public IModule LoadModule (Assembly assembly)
        {
            try
            {
                var moduleInstaller = FromAssembly.Instance (assembly);
                var modulecontainer = new WindsorContainer ();
                _mainContainer.AddChildContainer (modulecontainer);
                modulecontainer.Install (moduleInstaller);
                var module = modulecontainer.Resolve <IModule> ();
                if (! AssemblySource.Instance.Contains (assembly))
                    AssemblySource.Instance.Add (assembly);
                return module;
            }
            catch (Exception ex)
            {
                // TODO: good exception handling
                return null;
            }
        }
    }

A designer izhektitsya container shell (remember, we recorded specifically for this?). The method LoadModule get the installer of the module assembly. Create a separate container for the loaded module component. We register it as a child of the container shell. Apply module installer. We are trying to return an instance IModule. Caliburn.Micro inform the assembly that he applied the naming conventions for components in it.

And do not forget to register in our module loader ShellInstaller.

.Register (Component.For <ModuleLoader> ()

A bit of a "child container." The bottom line is that all of its components "see" the components of the parent container, in addition to their own, but not vice versa. Components of different child containers also do not know anything about each other. Get a shell of insulation modules and the modules from each other, but not modules of the shell - they see it.

Further implement IShell contract through which the modules will appeal to Shelley.

    class ShellImpl: IShell
    {
        private readonly ModuleLoader _loader;
        private readonly ShellViewModel _shellViewModel;
        public ShellImpl (ModuleLoader loader, ShellViewModel shellViewModel)
        {
            _loader = loader;
            _shellViewModel = shellViewModel;
        }
        public IList <ShellMenuItem> MenuItems {get {return _shellViewModel.MenuItems; }}
        public IModule LoadModule (Assembly assembly)
        {
            return _loader.LoadModule (assembly);
        }
    }

We register.

.Register (Component.For <IShell> (). ImplementedBy <ShellImpl> ())


Now we need to make sure that the modules are loaded when you start a shell. And where do they come from? In our example, the shell will look for the assembly modules near Shell.exe.

This functionality must be implemented in the method OnStartup:

 protected override void OnStartup (object sender, StartupEventArgs e)
        {
            var loader = _container.Resolve <ModuleLoader> ();
            var exeDir = Path.GetDirectoryName (Assembly.GetExecutingAssembly () Location.);
            var pattern = "* .dll";
            Directory
                .GetFiles (ExeDir, pattern)
                .Select (Assembly.LoadFrom)
                .Select (Loader.LoadModule)
                .Where (Module => module! = Null)
                .ForEach (Module => module.Init ());
            DisplayRootViewFor <ShellViewModel> ();
        }

All the shell is ready!

Writing module

Our test unit at startup will be added to the menu shell two points. The first item will display on the right side very simple form with inscription. The second - a form with a button, which can be used to reload the module by its assembly in the open file dialog. Following agreement on the names, create a folder 2 Views and ViewModels. Then fill them.

The first twist and twist model - are trivial:

<UserControl x: Class = "Module.Views.FirstView"
             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: d = "http://schemas.microsoft.com/expression/blend/2008"
             mc: Ignorable = "d"
             d: DesignHeight = "300" d: DesignWidth = "300">
    <Grid>
        <TextBlock HorizontalAlignment = "Center" VerticalAlignment = "Center" FontSize = "60"> Hello, I'm first! </ TextBlock>
    </ Grid>
</ UserControl>
    class FirstViewModel
    {
    }

The second twist is also not different complexity.

<UserControl x: Class = "Module.Views.SecondView"
             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: d = "http://schemas.microsoft.com/expression/blend/2008"
             mc: Ignorable = "d"
             d: DesignHeight = "300" d: DesignWidth = "300">
    <Grid>
            <Button x: Name = "Load" HorizontalAlignment = "Center" VerticalAlignment = "Center" FontSize = "50"> Load Module </ Button>
    </ Grid>
</ UserControl>


The second twist model implement load the selected module.

class SecondViewModel
    {
        private readonly IShell _shell;
        public SecondViewModel (IShell shell)
        {
            _shell = shell;
        }
        public void Load ()
        {
            var dlg = new OpenFileDialog ();
            if (dlg.ShowDialog (). GetValueOrDefault ())
            {
                var asm = Assembly.LoadFrom (dlg.FileName);
                var module = _shell.LoadModule (asm);
                if (module! = null)
                    module.Init ();
            }
        }
    }

We sell contract IModule. In the Init method of adding items to the shell menu.

class ModuleImpl: IModule
    {
        private readonly IShell _shell;
        private readonly FirstViewModel _firstViewModel;
        private readonly SecondViewModel _secondViewModel;
        public ModuleImpl (IShell shell, FirstViewModel firstViewModel, SecondViewModel secondViewModel)
        {
            _shell = shell;
            _firstViewModel = firstViewModel;
            _secondViewModel = secondViewModel;
        }
        public void Init ()
        {
            _shell.MenuItems.Add (new ShellMenuItem () {Caption = "First", ScreenViewModel = _firstViewModel});
            _shell.MenuItems.Add (new ShellMenuItem () {Caption = "Second", ScreenViewModel = _secondViewModel});
        }
    }
And the final touch - the installer.
public class ModuleInstaller: IWindsorInstaller
    {
        public void Install (IWindsorContainer container, IConfigurationStore store)
        {
            container
                .Register (Component.For <FirstViewModel> ())
                .Register (Component.For <SecondViewModel> ())
                .Register (. Component.For <IModule> () ImplementedBy <ModuleImpl> ());
        }
    }

Done!

Sources - on git-hub.

conclusion

In this article, we looked at creating a simple modular WPF-applications using frameworks and Castle.Windwsor Caliburn.Micro. Of course, many aspects have not been covered, some details are omitted, etc., otherwise would have been a book instead of articles. A more detailed information can be found on the official resources, and not only.

For all your questions gladly I try to answer.

Thank you for attention!

No comments:

Post a Comment