Routable Modules

6/26/2023

By: Shaun Walker

Developers who are familiar with Blazor understand the concept of "routable components" and "standard components". Routable components are identical to standard components except that they use an @page attribute to indicate a specific route that can be used to render the component in the UI. The @page attribute is used by the default Blazor router to create a route table to enable this functionality. However based on the fact that Oqtane uses its own custom router and utilizes a purely dynamic rendering model which leverages configuration stored in a database, "routable components" are not compatible with Oqtane (ie. the @page attribute is ignored).

That being said, Oqtane developers do need to be able to configure their sites with their custom modules and themes. Obviously there is an administrative UI which can be used to perform this task manually, however many developers prefer the consistency and predictability of static configuration. To address this need Oqtane supports an ISiteTemplate capability which allows developers to create custom site templates which can be used when creating a new site to provision pages, modules, content, etc... There is also an ISiteMigration capability which allows you to programmatically specify version-based migration logic to manipulate the configuration of your site as it evolves over time. The combination of these 2 capabilities provide some excellent opportunities for automation so that developers can avoid manual configuration.

While the two methods described above are excellent for automating the deployment of changes to higher level environments, they are not as well suited to the constant changes which can occur in a development environment. To address this specific challenge, Oqtane 4.0 has introduced the concept of "routable modules".

Routable modules provide a declarative method where a developer can specify the configuration details of how their module should be integrated into a site. Configuration is expressed through the IModule interface and includes all of the standard attributes for pages and modules. This static definition is utilized at startup to automatically reconcile the database with all of the specified configuration details and the result is that UI is fully configured with pages and modules.

From a technical perspective, this new capability is basically an extension of the existing ISiteTemplate capability which was already available with Site Templates. Leveraging a common foundation creates consistency and ensures that both capabilities are maintained and supported in tandem going forward. It also means that the ISiteTemplate feature received some improvements as well, as it did not yet support all of the attributes for Pages which were included in recent releases.

So let's explore the new "routable module" concept. Basically it utilizes the existing IModule interface and adds a new property named PageTemplates. The concept is that a module can declaratively describe the page(s) where the module will be utilized in a site. Let's start with the most simplistic example. Here we have a ModuleInfo.cs file for a module called "My Module". It is using the new PageTemplates capability to instruct the framework that it should be added to a "test" page automatically for the default development site (localhost:44357):

public class ModuleInfo : IModule
{
    public ModuleDefinition ModuleDefinition => new ModuleDefinition
    {
        Name = "My Module",
        PageTemplates = new List<PageTemplate>()
        {
            new PageTemplate { AliasName = "localhost:44357", Path = "test" }
        }
    };
}

Note that the logic for adding the page and module uses all of the same default property values that would be utilized if you created a page and added the module manually through the UI. The AliasName is mandatory so that the PageTemplate can be processed for the appropriate site in your installation (you can also specify "*" if you want it to be processed for all sites). The Path property is the key field for Pages and will be used to identity if the page already exists or not. Note that the logic will add a new page if it does not exist; however for performance reasons during startup and to mitigate the risk of overwriting changes made through the UI, it will not update the page automatically if it already exists. If you want to update the page you need to specify the Update property:

public class ModuleInfo : IModule
{
    public ModuleDefinition ModuleDefinition => new ModuleDefinition
    {
        Name = "My Module",
        PageTemplates = new List<PageTemplate>()
        {
            new PageTemplate { AliasName = "localhost:44357", Path = "test", Name = "Test Page", Update = true }
        }
    };
}

The ISiteMigration interface has a concept for targeting content migrations at a specific version of a site, and this can be utilized in PageTemplates as well. For example, if I wanted to add a page for version "1.0.1" of my site I can specify the Version property. This means that when the site version increases (ie. to 1.0.2 or higher) the PageTemplate will be ignored. This provides very granular control over your site content updates during startup.

public class ModuleInfo : IModule
{
    public ModuleDefinition ModuleDefinition => new ModuleDefinition
    {
        Name = "My Module",
        PageTemplates = new List<PageTemplate>()
        {
            new PageTemplate { AliasName = "localhost:44357", Version = "1.0.1", Path = "test", Name = "Test Page", Update = true }
        }
    };
}

Since a module can be included on multiple pages in a site, it is possible to define multiple PageTemplates. The following will create 2 pages and add the module to both:

public class ModuleInfo : IModule
{
    public ModuleDefinition ModuleDefinition => new ModuleDefinition
    {
        Name = "My Module",
        PageTemplates = new List<PageTemplate>()
        {
            new PageTemplate { AliasName = "localhost:44357", Path = "test1" },
            new PageTemplate { AliasName = "localhost:44357", Path = "test2" }
        }
    };
}

If you want to specify additional attributes for the page you can include them in the PageTemplate:

public class ModuleInfo : IModule
{
    public ModuleDefinition ModuleDefinition => new ModuleDefinition
    {
        Name = "My Module",
        PageTemplates = new List<PageTemplate>()
        {
        new PageTemplate
        {
                AliasName = "localhost:44357",
                Path = "home/child",
                Name = "Child",
                Parent = "home", // note that this is the Parent Path Path
                Title = "Child Page",
                Icon = "oi oi-person",
                ThemeType = "Oqtane.Themes.BlazorTheme.Default, Oqtane.Client",
                HeadContent = "",
                PermissionList = new List<Permission>()
                {
                    new Permission(PermissionNames.View, RoleNames.Admin, true),
                    new Permission(PermissionNames.View, RoleNames.Everyone, true),
                    new Permission(PermissionNames.Edit, RoleNames.Admin, true)
                }
            }
        }
    };
}

Since multiple instances of a module can be included on the same page in a site, it is possible to define multiple PageTemplateModules. The following will create 2 module instances on a "test" page:

public class ModuleInfo : IModule
{
    public ModuleDefinition ModuleDefinition => new ModuleDefinition
    {
        Name = "My Module",
        PageTemplates = new List<PageTemplate>()
        {
            new PageTemplate
            {
                AliasName = "localhost:44357",
                Path = "test",
                PageTemplateModules = new List<PageTemplateModule>()
                {
                    new PageTemplateModule { Title = "Module 1", Order = 1 },
                    new PageTemplateModule { Title = "Module 2", Order = 3 }
                }
            }
        }
    };
}

Note that although this capability is intended for scaffolding pages and module instances for the module where the IModule interface is defined, it is also possible to target a different type of module by explicitly specifying the ModuleDefinitionName. This allows you to scaffold pages for modules where you do not have the ability to modify the IModule interface (ie. third party modules).

public class ModuleInfo : IModule
{
    public ModuleDefinition ModuleDefinition => new ModuleDefinition
    {
        Name = "My Module",
        PageTemplates = new List<PageTemplate>()
        {
            new PageTemplate
            {
                AliasName = "localhost:44357",
                Path = "test",
                PageTemplateModules = new List<PageTemplateModule>()
                {
                    new PageTemplateModule
                    {
                        ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client",
                        Title = "HTML Module",
                        Content = "Some default content to import using the HTML/Text Module IPortable interface"
                    }
                }
            }
        }
    };
}

Based on these examples you should be able to see that there are a ton of new opportunities for automating your development efforts.


Do You Want To Be Notified When Blogs Are Published?
RSS