Building a simple "Hello World" module

Before we start, please look at the introduction to creating modules. The basic files needed to create a module are already described there.

Creating a project file

Start by creating a project file for your modules.

  1. Open the Smartstore Solution Smartstore.sln

  2. Right click on the Modules Folder in the Solution Explorer

  3. Add a New Project of type Class Library

  4. Name it MyOrg.HelloWorld

  5. Make sure the physical path of the project is Smartstore/src/Smartstore.Modules

Now alter MyOrg.HelloWorld.csproj to look like this:

<Project Sdk="Microsoft.NET.Sdk.Razor">
    <PropertyGroup>
        <Product>A Hello World module for Smartstore</Product>
        <OutputPath>..\..\Smartstore.Web\Modules\MyOrg.HelloWorld</OutputPath>
        <OutDir>$(OutputPath)</OutDir>
    </PropertyGroup>
</Project>

Adding module metadata

Add module.json next. For more information, refer to the manifest.

  1. Right click on the project in the Solution Explorer.

  2. Add / New Item / Javascript JSON Configuration File.

  3. Name it module.json

  4. Make another right click, select the Properties context item and change:

Property
Value

Build Action

Content

Copy to Output Directory

Copy if newer

5. Add the following content

module.json
{
  "$schema": "../module.schema.json",
  "Author": "My Org",
  "Group": "Admin",
  "SystemName": "MyOrg.HelloWorld",
  "FriendlyName": "Hello World",
  "Description": "This module says Hello World",
  "Version": "5.0",
  "MinAppVersion": "5.0",
  "Order": 1,
  "ResourceRootKey": "Plugins.MyOrg.HelloWorld",
  "ProjectUrl": "https://myorg.com"
}

Creating the module

Change the name of Class1.cs to Module.cs and add the following code:

Module.cs
using System.Threading.Tasks;
using Smartstore.Engine.Modularity;
using Smartstore.Http;

internal class Module : ModuleBase, IConfigurable
{
    public RouteInfo GetConfigurationRoute()
        => new("Configure", "HelloWorldAdmin", new { area = "Admin" });

    public override async Task InstallAsync(ModuleInstallationContext context)
    {
        // Saves the default state of a settings class to the database 
        // without overwriting existing values.
        //await TrySaveSettingsAsync<HelloWorldSettings>();
        
        // Imports all language resources for the current module from 
        // xml files in "Localization" directory (if any found).
        await ImportLanguageResourcesAsync();
        
        // VERY IMPORTANT! Don't forget to call.
        await base.InstallAsync(context);
    }

    public override async Task UninstallAsync()
    {
        // Deletes all "HelloWorldSettings" properties settings from the database.
        //await DeleteSettingsAsync<HelloWorldSettings>();
        
        // Deletes all language resource for the current module 
        // if "ResourceRootKey" is module.json is not empty.
        await DeleteLanguageResourcesAsync();
        
        // VERY IMPORTANT! Don't forget to call.
        await base.UninstallAsync();
    }
}

After compiling the project, the module is recognized by Smartstore and can be installed via Admin / Plugins / Manage Plugins / Hello World / Install.

There are two things to note here:

  1. Clicking on Configure will lead you to a 404 page. This is because no controller has been added and there is no action to handle the configuration route.

  2. The method to add default settings to the database and the method to remove them are commented out because the Settings class doesn't exist yet.

Adding a Settings class

For more detailed information on Settings visit the section Configuration. For this tutorial, just add a simple Setting class with one string property.

  1. Right click on the project in the Solution Explorer.

  2. Add a new folder. According to our guidelines we call it Configuration.

  3. Place a new class called HelloWorldSettings.cs in this folder.

Module.cs
using Smartstore.Core.Configuration;

namespace MyOrg.HelloWorld.Settings
{
    public class HelloWorldSettings : ISettings
    {
        public string Name { get; set; } = "John Smith";
    }
}

Now we can uncomment the corresponding lines in Module.cs, which saves the initial settings when the module is installed and removes them when the module is uninstalled. When the module is installed again, the HelloWorldSettings.Name setting is saved to the database with the default value of "John Smith".

Adding configuration

Now that you have a setting for your module, add this code to make the setting configurable. In Module.cs implement the interface IConfigurable, which has the method GetConfigurationRoute that returns RouteInfo. The method will be called when clicking on the Config button next to the module in the Plugin Management section of the shops administration area.

Module.cs
public RouteInfo GetConfigurationRoute()
    => new("Configure", "HelloWorldAdmin", new { area = "Admin" });

Using the RouteInfo, Smartstore looks for an action called Configure in a Controller named HelloWorldAdminController in the Admin area.

MVC parts

Controller

Add the controller:

  1. Right click on the project in the Solution Explorer.

  2. Add a new folder. According to our guidelines we call it Controllers.

  3. Place a new class called HelloWorldAdminController.cs in this folder.

HelloWorldAdminController.cs
using Microsoft.AspNetCore.Mvc;
using Smartstore.ComponentModel;
using Smartstore.Core.Security;
using MyOrg.HelloWorld.Models;
using MyOrg.HelloWorld.Settings;
using Smartstore.Web.Controllers;
using Smartstore.Web.Modelling.Settings;

namespace MyOrg.HelloWorld.Controllers
{
    public class HelloWorldAdminController : AdminController
    {
        [LoadSetting, AuthorizeAdmin]
        public IActionResult Configure(HelloWorldSettings settings)
        {
            var model = MiniMapper.Map<HelloWorldSettings, ConfigurationModel>(settings);
            return View(model);
        }

        [HttpPost, SaveSetting, AuthorizeAdmin]
        public IActionResult Configure(ConfigurationModel model, HelloWorldSettings settings)
        {
            if (!ModelState.IsValid)
            {
                return Configure(settings);
            }

            ModelState.Clear();
            MiniMapper.Map(model, settings);

            return RedirectToAction(nameof(Configure));
        }
    }
}

The configuration model is still missing, so there will be 3 errors right now. Notice the Area attribute the controller is decorated with. This means that all its actions are reachable only within this area. If you want to add actions to the module within another area don't forget to decorate these actions with the desired area or add another controller.

By inheriting from AdminController the admin Area attribute is added automatically.

According to the MVC pattern, there are two actions in this controller to handle the configure view. The first action is for the GET request and the second handles the POST request.

Attributes
Description

AuthorizeAdmin

Makes sure the current user has the right to access this view

LoadSetting

Loads the setting values of the settings class passed as the action parameter automatically from the database

SaveSetting

Saves the setting values of the settings class passed as the action parameter automatically to the database after the action was executed. So you can store your model values in the settings object.

Use the SaveSetting attribute in combination with the MiniMapper, which maps simple properties with the same name to each other. Calling MiniMapper.Map(model, settings) maps the Name property of the setting class to the Name property of the model class.

Refer to MiniMapper for more information.

If the ModelState isn’t valid, do a post back by returning Configure(settings) to display model validation errors, otherwise redirect to the GET action to prevent unnecessary form posts.

Model

As mentioned above, in this simple use case, the configuration model for the module settings is a simple copy of the settings class.

  1. Right click on the project in the Solution Explorer.

  2. Add a new folder. According to our guidelines we call it Models.

  3. Place a new class called ConfigurationModel.cs in this folder.

ConfigurationModel.cs
using Smartstore.Web.Modelling;

namespace MyOrg.HelloWorld.Models
{
    [LocalizedDisplay("Plugins.MyOrg.HelloWorld.")]
    public class ConfigurationModel : ModelBase
    {
        [LocalizedDisplay("*Name")]
        public string Name { get; set; }
    }
}

View

Now add the view that is rendered by the GET action of the controller.

  1. Right click on the project in the Solution Explorer.

  2. Add a new folder and call it Views/HelloWorldAdmin.

  3. Place a new Empty Razor View called Configure.cshtml in this folder.

Configure.cshtml
@model ConfigurationModel

@{
    Layout = "_ConfigureModule";
}

@* 
    Render "StoreScope" component if your setting class has 
    one or more multi-store enabled properties.
    It renders a store chooser that sets the current store scope.
    This way individual settings can be overridden on store level.
*@

@await Component.InvokeAsync("StoreScope")

@* Render the save button in admin toolbar *@
<widget target-zone="admin_button_toolbar_before">
    <button id="SaveConfigButton" type="submit" name="save" class="btn btn-warning" value="save">
        <i class="fa fa-check"></i>
        <span>@T("Admin.Common.Save")</span>
    </button>
</widget>

<form asp-action="Configure">
    <div asp-validation-summary="All"></div>
    <div class="adminContent">
        <div class="adminRow">
            <div class="adminTitle">
                <smart-label asp-for="Name" />
            </div>
            <div class="adminData">
                <setting-editor asp-for="Name"></setting-editor>
                <span asp-validation-for="Name"></span>
            </div>
        </div>
    </div>
</form>

To reduce the amount of using directives in the view, it is recommended to add a _ViewImports.cshtml file directly in the Views directory. Add the most important namespaces and the model namespace. This file includes the built-in Microsoft Tag Helpers as well as the Smartstore Tag Helpers.

_ViewImports.cshtml
@inherits Smartstore.Web.Razor.SmartRazorPage<TModel>

@using System
@using System.Globalization
@using Smartstore.Web.TagHelpers.Admin
@using Smartstore.Web.Rendering
@using MyOrg.HelloWorld.Models

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper Smartstore.Web.TagHelpers.Shared.*, Smartstore.Web.Common
@addTagHelper Smartstore.Web.TagHelpers.Admin.*, Smartstore.Web.Common

After building the module, you can click on the Configure button to store a value for the HelloWorldSettings.Name setting. It’s stored in the database by simply entering it in the provided input field of the configuration view.

Adding localization

If you take a look at the ConfigurationModel, you'll see that the properties are decorated with the LocalizedDisplay attribute. This is a way to add localized display values to describe the property. On property level, the attribute can either contain the full resource key [LocalizedDisplay("Plugins.MyOrg.HelloWorld.Name")] or inherit a part of the declaring class also decorated with the attribute.

The resource values are added using resource.*.xml XML files, where * represents the culture code of the target language:

  1. Right click on the project in the Solution Explorer.

  2. Add a new folder. The folder must be called Localization.

  3. Place a new XML file called resources.en-us.xml in this folder.

  4. Make another right click, select the Properties context item and change

Property
Value

Build Action

Content

Copy to Output Directory

Copy if newer

<Language Name="English" IsDefault="false" IsRightToLeft="false">
    <LocaleResource Name="Plugins.FriendlyName.MyOrg.HelloWorld" AppendRootKey="false">
        <Value>Hello World</Value>
    </LocaleResource>
    <LocaleResource Name="Plugins.Description.MyOrg.HelloWorld" AppendRootKey="false">
        <Value>This plugin says Hello World.</Value>
    </LocaleResource>

    <LocaleResource Name="Plugins.MyOrg.HelloWorld" AppendRootKey="false">
        <Children>
            <LocaleResource Name="Name">
                <Value>Name to greet</Value>
            </LocaleResource>
            <LocaleResource Name="Name.Hint">
                <Value>Enter the name of the person to be greeted.</Value>
            </LocaleResource>
        </Children>
    </LocaleResource>
</Language>

After building the module you can press the button Update resources to update the newly added localized resources from your XML file.

Say Hello

Now that you can configure the name of the person to be greeted, add another controller, a model and a view for the public action.

  1. Right click on the Controllers directory in the Solution Explorer.

  2. Add a new class called HelloWorldController.cs

Controllers/HelloWorldController.cs
using Microsoft.AspNetCore.Mvc;
using MyOrg.HelloWorld.Models;
using MyOrg.HelloWorld.Settings;
using Smartstore.ComponentModel;
using Smartstore.Web.Controllers;
using Smartstore.Web.Modelling.Settings;

namespace MyOrg.HelloWorld.Controllers
{
    public class HelloWorldController : PublicController
    {
        [LoadSetting]
        public IActionResult PublicInfo(HelloWorldSettings settings)
        {
            var model = MiniMapper.Map<HelloWorldSettings, PublicInfoModel>(settings);
            return View(model);
        }
    }
}
  1. Right click on the Models directory in the Solution Explorer.

  2. Add a new class called PublicInfoModel.cs

Models/PublicInfoModel.cs
using Smartstore.Web.Modelling;

namespace MyOrg.HelloWorld.Models
{
    public class PublicInfoModel : ModelBase
    {
        public string Name { get; set; }
    }
}
  1. Right click on the Views directory in the Solution Explorer.

  2. Add a new folder named HelloWorld

  3. Add a new Razor View called PublicInfo.cshtml

Views\HelloWorld\PublicInfo.cshtml
@model PublicInfoModel

@{
    Layout = "_Layout";
}

<div>
    Hello @Model.Name
</div>

The public view will be displayed when opening the URL: http://localhost:59318/helloworld/publicInfo

Conclusion

Open the project file and remove all ItemGroup properties because they are not needed in the Smartstore build process.

Now you have built a simple module that stores a setting and renders its value in the frontend, when accessing the route helloworld / publicinfo. Of course this is the starting point on the way to build more complex modules by using: Action Filters, own DataContext and View Components, which are rendered in Widget Zones.

The code for this tutorial can be found in the examples repository.

Last updated

Was this helpful?