Module overview in Prism framework

summary

  when developing WPF programs using Prism framework, a very important core idea is to build modular applications. Modules are decoupled from each other, and modules can be loosely combined. Before we have an understanding of the modular design idea in Prism, let's take a look at the following figure, Through this diagram, we can have a clearer understanding of the whole Module.

  from the above figure, we know that the Module is a concept located at the lower layer of the Shell. The Module includes views, Services and other application infrastructures. What are the stages of using the whole Module in the whole Prism framework? What is the focus of each stage? Next, let's use a diagram to understand the whole process of Module development and use in Prism.

  the whole process includes: registration / discovery module, loading module and initialization module.

  • Registration / discovery module. The modules loaded for a specific application at run time are defined in the module directory, which contains information about the modules to be loaded, their location and loading order.
  • Load the module. The assembly containing the module will be loaded into memory, and this stage may require retrieving the module from a remote location or local directory.
  • Initialize the module. Then initialize the module, which means creating instances of module classes and calling their initialize methods through the IModule interface.
      with the understanding process outlined above, we will analyze the whole process step by step by reading the source code.

Source code analysis

1 IModuleInfo interface

  this is the most basic interface in the whole Module. It is used to describe the information contained in the current Module. It should be noted that the IModuleInfo interface inherits the empty interface of an IModuleCatalogItem. This is mainly used as an important identification when adding IModuleInfo to the ModuleCatalog later. In addition, IModuleInfo defines which modules the current Module depends on? (described by dependson. For example, if ModuleA depends on ModuleB, ModuleB must be initialized before ModuleA). There are two main initializationmodes defined in prism 8: 1. When available (the default mode is automatically loaded after the application is started), 2 OnDemand (it is not loaded by default. You need to dynamically call LoadModule in the code to load it as needed). Both ModuleName and ModuleType are string types, which are used to describe the specific name and type information of the Module. The Ref field is special. For example, our current Module information needs to be downloaded locally through remote and then loaded dynamically. The last one is ModuleState, which is used to describe the status information of the current Module. This will be described in detail later

/// <summary>
    /// Set of properties for each Module
    /// </summary>
    public interface IModuleInfo : IModuleCatalogItem
    {
        /// <summary>
        /// The module names this instance depends on.
        /// </summary>
        Collection<string> DependsOn { get; set; }

        /// <summary>
        /// Gets or Sets the <see cref="InitializationMode" />
        /// </summary>
        InitializationMode InitializationMode { get; set; }

        /// <summary>
        /// The name of the module
        /// </summary>
        string ModuleName { get; set; }

        /// <summary>
        /// The module's type
        /// </summary>
        string ModuleType { get; set; }

        /// <summary>
        /// A string ref is a location reference to load the module as it may not be already loaded in the Appdomain in some cases may need to be downloaded.
        /// </summary>
        /// <Remarks>
        /// This is only used for WPF
        /// </Remarks>
        string Ref { get; set; }

        /// <summary>
        /// Gets or Sets the current <see cref="ModuleState" />
        /// </summary>
        ModuleState State { get; set; }
    }

  Module status information

/// <summary>
    /// Defines the states a <see cref="IModuleInfo"/> can be in, with regards to the module loading and initialization process. 
    /// </summary>
    public enum ModuleState
    {
        /// <summary>
        /// Initial state for <see cref="IModuleInfo"/>s. The <see cref="IModuleInfo"/> is defined, 
        /// but it has not been loaded, retrieved or initialized yet.
        /// </summary>
        NotStarted,

        /// <summary>
        /// The assembly that contains the type of the module is currently being loaded.
        /// </summary>
        /// <remarks>
        /// Used in Wpf to load a module dynamically
        /// </remarks>
        LoadingTypes,

        /// <summary>
        /// The assembly that holds the Module is present. This means the type of the <see cref="IModule"/> can be instantiated and initialized. 
        /// </summary>
        ReadyForInitialization,

        /// <summary>
        /// The module is currently Initializing, by the <see cref="IModuleInitializer"/>
        /// </summary>
        Initializing,

        /// <summary>
        /// The module is initialized and ready to be used. 
        /// </summary>
        Initialized
    }

2 IModuleCatalog interface

  as the name implies, it is the Module directory. Let's look at this interface. The key point is to maintain a collection of IModuleInfo, and add, initialize and obtain the dependent collection properties of a certain ModuleInfo for this collection. There is a CompleteListWithDependencies. We don't know its purpose from the name and comments, Later, we will analyze the function of this method through the specific source code.

/// <summary>
    /// This is the expected catalog definition for the ModuleManager. 
    /// The ModuleCatalog holds information about the modules that can be used by the 
    /// application. Each module is described in a ModuleInfo class, that records the 
    /// name, type and location of the module. 
    /// </summary>
    public interface IModuleCatalog
    {
        /// <summary>
        /// Gets all the <see cref="IModuleInfo"/> classes that are in the <see cref="IModuleCatalog"/>.
        /// </summary>
        IEnumerable<IModuleInfo> Modules { get; }

        /// <summary>
        /// Return the list of <see cref="IModuleInfo"/>s that <paramref name="moduleInfo"/> depends on.
        /// </summary>
        /// <param name="moduleInfo">The <see cref="IModuleInfo"/> to get the </param>
        /// <returns>An enumeration of <see cref="IModuleInfo"/> that <paramref name="moduleInfo"/> depends on.</returns>
        IEnumerable<IModuleInfo> GetDependentModules(IModuleInfo moduleInfo);

        /// <summary>
        /// Returns the collection of <see cref="IModuleInfo"/>s that contain both the <see cref="IModuleInfo"/>s in 
        /// <paramref name="modules"/>, but also all the modules they depend on. 
        /// </summary>
        /// <param name="modules">The modules to get the dependencies for.</param>
        /// <returns>
        /// A collection of <see cref="IModuleInfo"/> that contains both all <see cref="IModuleInfo"/>s in <paramref name="modules"/>
        /// and also all the <see cref="IModuleInfo"/> they depend on.
        /// </returns>
        IEnumerable<IModuleInfo> CompleteListWithDependencies(IEnumerable<IModuleInfo> modules);

        /// <summary>
        /// Initializes the catalog, which may load and validate the modules.
        /// </summary>
        void Initialize();

        /// <summary>
        /// Adds a <see cref="IModuleInfo"/> to the <see cref="IModuleCatalog"/>.
        /// </summary>
        /// <param name="moduleInfo">The <see cref="IModuleInfo"/> to add.</param>
        /// <returns>The <see cref="IModuleCatalog"/> for easily adding multiple modules.</returns>
        IModuleCatalog AddModule(IModuleInfo moduleInfo);
    }

2.1 analysis of completelistwithdependencies method

  let's see the role of the CompleteListWithDependencies through a unit test.

[Fact]
        public void CanCompleteListWithTheirDependencies()
        {
            // A <- B <- C
            var moduleInfoA = CreateModuleInfo("A");
            var moduleInfoB = CreateModuleInfo("B", "A");
            var moduleInfoC = CreateModuleInfo("C", "B");
            var moduleInfoOrphan = CreateModuleInfo("X", "B");

            List<ModuleInfo> moduleInfos = new List<ModuleInfo>
                                               {
                                                   moduleInfoA
                                                   , moduleInfoB
                                                   , moduleInfoC
                                                   , moduleInfoOrphan
                                               };
            var moduleCatalog = new ModuleCatalog(moduleInfos);

            var dependantModules = moduleCatalog.CompleteListWithDependencies(new[] { moduleInfoC });

            Assert.Equal(3, dependantModules.Count());
            Assert.Contains(moduleInfoA, dependantModules);
            Assert.Contains(moduleInfoB, dependantModules);
            Assert.Contains(moduleInfoC, dependantModules);
        }

  let's look at the dependencies between modules A, B, C and X.

graph LR C --> B B --- A X --> B

  according to the unit test CompleteListWithDependencies above, when inputting moduleC as a parameter, you can find the relationship between moduleC's entire chain of dependent modules C -- > B -- > A. through this example, you should know the details of the last doubt.

2.2 Initialize method analysis

  the core method in IModuleCatalog is to initialize ModuleCatalog. Let's take a look at the definition of initialization method in the base class ModuleCatalogBase.

/// <summary>
        /// Initializes the catalog, which may load and validate the modules.
        /// </summary>
        /// <exception cref="ModularityException">When validation of the <see cref="ModuleCatalogBase"/> fails, because this method calls <see cref="Validate"/>.</exception>
        public virtual void Initialize()
        {
            if (!_isLoaded)
            {
                Load();
            }

            Validate();
        }

 /// <summary>
        /// Loads the catalog if necessary.
        /// </summary>
        public virtual void Load()
        {
            _isLoaded = true;
            InnerLoad();
        }

/// <summary>
        /// Does the actual work of loading the catalog.  The base implementation does nothing.
        /// </summary>
        protected virtual void InnerLoad()
        {
        }

  here, the base class finally calls an empty virtual method InnerLoad to load the final Modules. We know that prism 8 provides a variety of module discovery methods by default, such as: 1 through app Config. 2 search through Directory Folder. 3 dynamically load by dynamically parsing xaml files. Here we analyze the three module discovery methods.

2.2.1 ConfigurationModuleCatalog

/// <summary>
    /// A catalog built from a configuration file.
    /// </summary>
    public class ConfigurationModuleCatalog : ModuleCatalog
    {
        /// <summary>
        /// Builds an instance of ConfigurationModuleCatalog with a <see cref="ConfigurationStore"/> as the default store.
        /// </summary>
        public ConfigurationModuleCatalog()
        {
            Store = new ConfigurationStore();
        }

        /// <summary>
        /// Gets or sets the store where the configuration is kept.
        /// </summary>
        public IConfigurationStore Store { get; set; }

        /// <summary>
        /// Loads the catalog from the configuration.
        /// </summary>
        protected override void InnerLoad()
        {
            if (Store == null)
            {
                throw new InvalidOperationException(Resources.ConfigurationStoreCannotBeNull);
            }

            EnsureModulesDiscovered();
        }

        private void EnsureModulesDiscovered()
        {
            ModulesConfigurationSection section = Store.RetrieveModuleConfigurationSection();

            if (section != null)
            {
                foreach (ModuleConfigurationElement element in section.Modules)
                {
                    IList<string> dependencies = new List<string>();

                    if (element.Dependencies.Count > 0)
                    {
                        foreach (ModuleDependencyConfigurationElement dependency in element.Dependencies)
                        {
                            dependencies.Add(dependency.ModuleName);
                        }
                    }

                    ModuleInfo moduleInfo = new ModuleInfo(element.ModuleName, element.ModuleType)
                    {
                        Ref = GetFileAbsoluteUri(element.AssemblyFile),
                        InitializationMode = element.StartupLoaded ? InitializationMode.WhenAvailable : InitializationMode.OnDemand
                    };
                    moduleInfo.DependsOn.AddRange(dependencies.ToArray());
                    AddModule(moduleInfo);
                }
            }
        }
    }

  maybe we just look at the code in this part and don't quite understand the specific usage of the above code, but let's take a look at the following app You can understand the meaning of the configuration in the config project.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <section name="modules" type="Prism.Modularity.ModulesConfigurationSection, Prism.Wpf" />
  </configSections>
  <startup>
  </startup>
  <modules>
    <module assemblyFile="ModuleA.dll" moduleType="ModuleA.ModuleAModule, ModuleA, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" moduleName="ModuleAModule" startupLoaded="True" />
  </modules>
</configuration>

  the above code parses the modules nodes under Configuration one by one through a ConfigurationStore and adds them to the modules collection maintained in the parent class one by one. With this as a foreshadowing, the latter two methods can draw inferences from one example, but we still analyze them one by one.
  finally, we need to rewrite the CreateModuleCatalog() method in PrismApplication to let Shell know how to load specific modules. Let's look at the following code.

/// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : PrismApplication
    {
        protected override Window CreateShell()
        {
            return Container.Resolve<MainWindow>();
        }

        protected override void RegisterTypes(IContainerRegistry containerRegistry)
        {

        }

        protected override IModuleCatalog CreateModuleCatalog()
        {
            return new ConfigurationModuleCatalog();
        }
    }

2.2.2 ConfigurationModuleCatalog

  this usage loads the dll inherited from the IModule interface from a specific path, then parses the module information in each dll one by one through reflection, and then dynamically loads it into the Modules collection in the IModuleCatalog, It should be noted that these Modules loaded dynamically through reflection belong to a sub application domain called discovery region, so as to achieve the effect of isolation from the application domain in Prism main framework. This part is not listed here because there are too many codes. If necessary, you can read the design of the source code in detail.
  in addition, we need to rewrite the CreateModuleCatalog() method in prism application. However, the specific DirectoryModuleCatalog module knows which path to find the corresponding module. The specific implementation is as follows.

 /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : PrismApplication
    {
        protected override Window CreateShell()
        {
            return Container.Resolve<MainWindow>();
        }

        protected override void RegisterTypes(IContainerRegistry containerRegistry)
        {

        }

        protected override IModuleCatalog CreateModuleCatalog()
        {
            return new DirectoryModuleCatalog() { ModulePath = @".\Modules" };
        }
    }

2.2.3 XamlModuleCatalog

  this part is dynamically parsed through a XAML file according to the code comments. Let's take a look at the specific implementation.

 /// <summary>
    /// A catalog built from a XAML file.
    /// </summary>
    public class XamlModuleCatalog : ModuleCatalog
    {
        private readonly Uri _resourceUri;

        private const string _refFilePrefix = "file://";
        private int _refFilePrefixLength = _refFilePrefix.Length;

        /// <summary>
        /// Creates an instance of a XamlResourceCatalog.
        /// </summary>
        /// <param name="fileName">The name of the XAML file</param>
        public XamlModuleCatalog(string fileName)
            : this(new Uri(fileName, UriKind.Relative))
        {
        }

        /// <summary>
        /// Creates an instance of a XamlResourceCatalog.
        /// </summary>
        /// <param name="resourceUri">The pack url of the XAML file resource</param>
        public XamlModuleCatalog(Uri resourceUri)
        {
            _resourceUri = resourceUri;
        }

        /// <summary>
        /// Loads the catalog from the XAML file.
        /// </summary>
        protected override void InnerLoad()
        {
            var catalog = CreateFromXaml(_resourceUri);

            foreach (IModuleCatalogItem item in catalog.Items)
            {
                if (item is ModuleInfo mi)
                {
                    if (!string.IsNullOrWhiteSpace(mi.Ref))
                        mi.Ref = GetFileAbsoluteUri(mi.Ref);
                }
                else if (item is ModuleInfoGroup mg)
                {
                    if (!string.IsNullOrWhiteSpace(mg.Ref))
                    {
                        mg.Ref = GetFileAbsoluteUri(mg.Ref);
                        mg.UpdateModulesRef();
                    }
                    else
                    {
                        foreach (var module in mg)
                        {
                            module.Ref = GetFileAbsoluteUri(module.Ref);
                        }
                    }
                }

                Items.Add(item);
            }
        }

        /// <inheritdoc />
        protected override string GetFileAbsoluteUri(string path)
        {
            //this is to maintain backwards compatibility with the old file:/// and file:// syntax for Xaml module catalog Ref property
            if (path.StartsWith(_refFilePrefix + "/", StringComparison.Ordinal))
            {
                path = path.Substring(_refFilePrefixLength + 1);
            }
            else if (path.StartsWith(_refFilePrefix, StringComparison.Ordinal))
            {
                path = path.Substring(_refFilePrefixLength);
            }

            return base.GetFileAbsoluteUri(path);
        }

        /// <summary>
        /// Creates a <see cref="ModuleCatalog"/> from XAML.
        /// </summary>
        /// <param name="xamlStream"><see cref="Stream"/> that contains the XAML declaration of the catalog.</param>
        /// <returns>An instance of <see cref="ModuleCatalog"/> built from the XAML.</returns>
        private static ModuleCatalog CreateFromXaml(Stream xamlStream)
        {
            if (xamlStream == null)
            {
                throw new ArgumentNullException(nameof(xamlStream));
            }

            return XamlReader.Load(xamlStream) as ModuleCatalog;
        }

        /// <summary>
        /// Creates a <see cref="ModuleCatalog"/> from a XAML included as an Application Resource.
        /// </summary>
        /// <param name="builderResourceUri">Relative <see cref="Uri"/> that identifies the XAML included as an Application Resource.</param>
        /// <returns>An instance of <see cref="ModuleCatalog"/> build from the XAML.</returns>
        private static ModuleCatalog CreateFromXaml(Uri builderResourceUri)
        {
            var streamInfo = System.Windows.Application.GetResourceStream(builderResourceUri);

            if ((streamInfo != null) && (streamInfo.Stream != null))
            {
                return CreateFromXaml(streamInfo.Stream);
            }

            return null;
        }
    }

  in the same part, we need to use a specific example to understand the meaning of the code. Let's take a look at the following example.

<m:ModuleCatalog xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:m="clr-namespace:Prism.Modularity;assembly=Prism.Wpf">

    <m:ModuleInfo ModuleName="ModuleAModule" 
                  ModuleType="ModuleA.ModuleAModule, ModuleA, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />

</m:ModuleCatalog>

  the core of the above implementation is through xamlreader Load to load the data stream to read the complete content. This also needs to be in app CS rewrite the CreateModuleCatalog() method to let the sub module know where to read the xaml file.

/// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : PrismApplication
    {
        protected override Window CreateShell()
        {
            return Container.Resolve<MainWindow>();
        }

        protected override void RegisterTypes(IContainerRegistry containerRegistry)
        {

        }

        protected override IModuleCatalog CreateModuleCatalog()
        {
            return new XamlModuleCatalog(new Uri("/Modules;component/ModuleCatalog.xaml", UriKind.Relative));
        }
    }

summary

  this article mainly introduces the two interfaces of IModuleInfo and IModuleCatalog in the Module, and summarizes the different loading methods of modules in Prism. Of course, some details are omitted here because there are too many codes, and the specific details need to understand the source code. This article is mainly analyzed through some processes and overall frameworks, There will be a more in-depth introduction in the following middle and next chapters.

Keywords: prism

Added by Carline on Mon, 17 Jan 2022 06:14:52 +0200