Blog

Read About What We Love: Web Development & Technologies

Plugin Development in nopCommerce 4.0

Plugin Development in nopCommerce 4.0

Plugin development in nopCommerce 4.0 is very easy owing to its pluggable modular/layered architecture. This key feature allows additional functionality and presentation elements that can be dynamically added to the application during runtime. This way, store owners can easily create and manage their nopCommerce based e-store.

In this blog, we explain the steps required to create widgets-based nopCommerce plugins for nopCommerce version 4.0.

1. Add a new .Net Core class library project under Plugins folder of nopCommerce 4.0 solution and name the plugin as per your plugin functionality. Eg: Nop.Plugin.Widgets.FollowPrice

2. Edit the plugin project file (*.csproj) in any editor eg: notepad and replace the content with the following content.

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net461</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>..\..\Presentation\Nop.Web\Plugins\PLUGIN_OUTPUT_DIRECTORY</OutputPath>
<OutDir>$(OutputPath)</OutDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>..\..\Presentation\Nop.Web\Plugins\PLUGIN_OUTPUT_DIRECTORY</OutputPath>
<OutDir>$(OutputPath)</OutDir>
</PropertyGroup>
<!-- This target execute after "Build" target -->
<Target Name="NopTarget" AfterTargets="Build">
<!-- Delete unnecessary libraries from plugins path -->
<MSBuild Projects="$(MSBuildProjectDirectory)\..\..\Build\ClearPluginAssemblies.proj"
Properties="PluginPath=$(MSBuildProjectDirectory)\$(OutDir)" Targets="NopClear" />
</Target>
</Project>

Replace ‘PLUGIN_OUTPUT_DIRECTORY’ in the above code with your plugin output directory.

3. Copy ‘plugin.json’ file from other plugin project to your plugin project. Edit ‘plugin.json’ file as per you plugin details. Sample ‘plugin.json’ file looks like below.

{
  "Group": "Widgets",
  "FriendlyName": "Follow Price",
  "SystemName": "Widgets.FollowPrice",
  "Version": "1.0",
  "SupportedVersions": [ "4.00" ],
  "Author": "Gaja Digital Agency",
  "DisplayOrder": 1,
  "FileName": "Nop.Plugin.Widgets.FollowPrice.dll",
  "Description": "This Plugin add the Follow Price button on the product page"
}

4. Create a settings class in your plugin project derived from ‘ISettings‘ with required properties sample settings file looks like below.

public class FollowPriceSettings : ISettings
    {
        public string APIKey { get; set; }
        public string CSSClassName { get; set; }
    }

5. Create a class file in your plugin project derived from ‘BasePlugin, IWidgetPlugin’. override ‘Install()’, ‘Uninstall()’ and ‘GetConfigurationPageUrl()’ methods of ‘BasePlugin’. Implement ‘GetPublicViewComponent’ and ‘GetWidgetZones’ methods of ‘IWidgetPlugin’ sample class file looks like below.

public class FollowPricePlugin : BasePlugin, IWidgetPlugin
    {
        private readonly ISettingService _settingService;
        private readonly IWebHelper _webHelper;
        public FollowPricePlugin(ISettingService settingService, IWebHelper webHelper)
        {
            this._settingService = settingService;
            this._webHelper = webHelper;
        }
 
        public void GetPublicViewComponent(string widgetZone, out string viewComponentName)
        {
            viewComponentName = "WidgetsFollowPrice";
        }
 
        public IList<string> GetWidgetZones()
        {
            return new List<string> { "productdetails_inside_overview_buttons_before" };
        }
 
        public override string GetConfigurationPageUrl()
        {
            return _webHelper.GetStoreLocation() + "Admin/WidgetsFollowPrice/Configure";
        }
 
        public override void Install()
        {
            var settings = new FollowPriceSettings
            {
                APIKey = "",
                CSSClassName = ""
            };
 
            _settingService.SaveSetting(settings);
         
            this.AddOrUpdatePluginLocaleResource("Plugins.Widgets.FollowPrice.APIKey", "API Key");
            this.AddOrUpdatePluginLocaleResource("Plugins.Widgets.FollowPrice.APIKey.Hint", "Enter Follow Price API Key");
            this.AddOrUpdatePluginLocaleResource("Plugins.Widgets.FollowPrice.CSSClass", "CSS Class Name");
            this.AddOrUpdatePluginLocaleResource("Plugins.Widgets.FollowPrice.CSSClass.Hint", "Enter CSS class name to override Follow price button style."); 

            base.Install();
        }
        public override void Uninstall()
        {
            _settingService.DeleteSetting<FollowPriceSettings>();
           
            this.DeletePluginLocaleResource("Plugins.Widgets.FollowPrice.APIKey");
            this.DeletePluginLocaleResource("Plugins.Widgets.FollowPrice.APIKey.Hint");
            this.DeletePluginLocaleResource("Plugins.Widgets.FollowPrice.CSSClass");
            this.DeletePluginLocaleResource("Plugins.Widgets.FollowPrice.CSSClass.Hint");
           
            base.Uninstall();
        }
    }

6. Create folders like ‘Components’, ‘Controllers’, ‘Models’ and ‘Views’ in your project file.

7. Copy ‘_ViewImports.cshtml’ file from other plugins projects View folder to ‘View’ folder of your project and sample ‘_ViewImports.cshtml’ file content looks like below.

@inherits Nop.Web.Framework.Mvc.Razor.NopRazorPage<TModel>
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelper
@addTagHelper *, Nop.Web.Framework
@using Microsoft.AspNetCore.Mvc.ViewFeatures
@using Nop.Web.Framework.UI
@using Nop.Web.Framework.Extensions
@using System.Text.Encodings.Web

8. Create a Configuration model under ‘Models’ folder, derived from ‘BaseNopModel’ with configuration properties required by your plugin. Sample configuration model looks like below.

public class ConfigurationModel : BaseNopModel
 {
        public int ActiveStoreScopeConfiguration { get; set; }
 
        [NopResourceDisplayName("Plugins.Widgets.FollowPrice.APIKey")]
        public string APIKey { get; set; }
        public bool APIKey_OverrideForStore { get; set; }
 
        [NopResourceDisplayName("Plugins.Widgets.FollowPrice.CSSClass")]
        public string CSSClass { get; set; }
        public bool CSSClass_OverrideForStore { get; set; }
  }

9. Create a Public info model under ‘Models’ folder derived from ‘BaseNopModel’ with properties required by your public info view component view file. Sample public info model looks like below.

public class PublicInfoModel : BaseNopModel
{
        public string APIKey { get; set; }
        public string CSSClassName { get; set; }
        public int ProductId { get; set; }
        public string ProductName { get; set; }
        public string ProductUrl { get; set; }
        public string ProductImageUrl { get; set; }
        public decimal productPrice { get; set; }
        public string CurrencyCode { get; set; }
        public int ProductAvailable { get; set; }
        public int UserId { get; set; }
        public string Category { get; set; }
        public string SubCategory { get; set; }
        public string LanguageCode { get; set; }
 }

10. Create a controller class under ‘Controllers’ folder of your plugin project derived from ‘BasePluginController’. Add Action methods to get and save configuration settings. Sample plugin controller method looks like below.

[Area(AreaNames.Admin)]
    public class WidgetsFollowPriceController : BasePluginController
    {
        private readonly IWorkContext _workContext;
        private readonly IStoreService _storeService;
        private readonly IPermissionService _permissionService;
        private readonly ISettingService _settingService;
        private readonly ILocalizationService _localizationService;
 
 
        public WidgetsFollowPriceController(IWorkContext workContext, IStoreService storeService,
            IPermissionService permissionService, ISettingService settingService,
            ILocalizationService localizationService)
        {
            this._workContext = workContext;
            this._storeService = storeService;
            this._permissionService = permissionService;
            this._settingService = settingService;
            this._localizationService = localizationService;
        }
 
        public IActionResult Configure()
        {
            if (!_permissionService.Authorize(StandardPermissionProvider.ManageWidgets))
                return AccessDeniedView();
 
            var storeScope = this.GetActiveStoreScopeConfiguration(_storeService, _workContext);
            var followPriceSettings = _settingService.LoadSetting<FollowPriceSettings>(storeScope);
 
            var model = new ConfigurationModel
            {
                APIKey = followPriceSettings.APIKey,
                CSSClass = followPriceSettings.CSSClassName
            };
 
            if(storeScope > 0)
            {
                model.APIKey_OverrideForStore = _settingService.SettingExists(followPriceSettings, x => x.APIKey, storeScope);
                model.APIKey_OverrideForStore = _settingService.SettingExists(followPriceSettings, x => x.CSSClassName, storeScope);
            }
 
            return View("~/Plugins/Widgets.FollowPrice/Views/Configure.cshtml", model);
        }
 
        [HttpPost]
        public IActionResult Configure(ConfigurationModel model)
        {
            if (!_permissionService.Authorize(StandardPermissionProvider.ManageWidgets))
                return AccessDeniedView();
 
            var storeScope = this.GetActiveStoreScopeConfiguration(_storeService, _workContext);
            var followPriceSettings = _settingService.LoadSetting<FollowPriceSettings>(storeScope);
 
            followPriceSettings.APIKey = model.APIKey;
            followPriceSettings.CSSClassName = model.CSSClass;
 
            _settingService.SaveSettingOverridablePerStore(followPriceSettings, x => x.APIKey, model.APIKey_OverrideForStore, storeScope, false);
            _settingService.SaveSettingOverridablePerStore(followPriceSettings, x => x.CSSClassName, model.CSSClass_OverrideForStore, storeScope, false);
             _settingService.ClearCache();
             SuccessNotification(_localizationService.GetResource("Admin.Plugins.Saved"));
           return Configure();
        }
    }

11. Create a config view file under ‘Views’ folder of your plugin, to update plugin settings from the admin back office. Sample configuration view file looks like below. Configuration view file layout should be "_ConfigurePlugin"

@model Nop.Plugin.Widgets.FollowPrice.Models.ConfigurationModel
@{
    Layout = "_ConfigurePlugin";
}
 
@await Component.InvokeAsync("StoreScopeConfiguration")
<form asp-controller="WidgetsFollowPrice" asp-action="Configure" method="post">
 
    <div class="panel-group">
        <div class="panel panel-default">
            <div class="panel-heading">
            </div>
            <div class="panel-body">
 
                <div class="form-group">
                    <div class="col-md-3">
                        <nop-override-store-checkbox asp-for="APIKey_OverrideForStore" asp-input="APIKey" asp-store-scope="@Model.ActiveStoreScopeConfiguration" />
                        <nop-label asp-for="APIKey" />
                    </div>
                    <div class="col-md-9">
                        <nop-editor asp-for="APIKey" />
                        <span asp-validation-for="APIKey"></span>
                    </div>
                </div> 
                <div class="form-group">
                    <div class="col-md-3">
                        <nop-override-store-checkbox asp-for="CSSClass_OverrideForStore" asp-input="CSSClass" asp-store-scope="@Model.ActiveStoreScopeConfiguration" />
                        <nop-label asp-for="CSSClass" />
                    </div>
                    <div class="col-md-9">
                        <nop-editor asp-for="CSSClass" />
                        <span asp-validation-for="CSSClass"></span>
                    </div>
                </div>
            </div>
        </div>
 
        <div class="panel panel-default">
            <div class="panel-body">
                <div class="form-group">
                    <div class="col-md-9 col-md-offset-3">
                        <input type="submit" name="save" class="btn bg-blue" value="@T("Admin.Common.Save")" />
                    </div>
                </div>
            </div>
        </div>
    </div>
</form>

12. Create view component class file under ‘Components’ folder of your plugin and implement ‘Invoke()’ as shown in the sample code below.

[ViewComponent(Name = "WidgetsFollowPrice")]
    public class WidgetsFollowPriceViewComponent : NopViewComponent
    {
        private readonly IStoreContext _storeContext;
        private readonly ISettingService _settingService;
        private readonly IProductService _productService;
        private readonly IProductModelFactory _productModelFactory;
        private readonly IWebHelper _webHelper;
        private readonly IWorkContext _workContext;
 
        public WidgetsFollowPriceViewComponent(IStoreContext storeContext,
            ISettingService settingService, IProductService productService,
            IProductModelFactory productModelFactory, IWebHelper webHelper,
            IWorkContext workContext)
        {
            this._storeContext = storeContext;
            this._settingService = settingService;
            this._productService = productService;
            this._productModelFactory = productModelFactory;
            this._webHelper = webHelper;
            this._workContext = workContext;
        }
 
        public IViewComponentResult Invoke(string widgetZone, object additionalData)
        {
            var followPriceSettings = _settingService.LoadSetting<FollowPriceSettings>(_storeContext.CurrentStore.Id);
 
            var model = new PublicInfoModel
            {
                APIKey = followPriceSettings.APIKey,
                CSSClassName = followPriceSettings.CSSClassName
            };
           
            if (additionalData != null)
            {
                model.ProductId = (int)additionalData;
                model.LanguageCode = _workContext.WorkingLanguage.LanguageCulture;
                model.UserId = _workContext.CurrentCustomer != null ? _workContext.CurrentCustomer.Id : 0;
                if(model.ProductId > 0)
                {
                    var product = _productService.GetProductById(model.ProductId);
                    if(product != null && !product.Deleted)
                    {
                        var productDetails = _productModelFactory.PrepareProductDetailsModel(product);
                        model.ProductName = productDetails.Name;
                        model.ProductUrl = _webHelper.GetStoreHost(_webHelper.IsCurrentConnectionSecured()) + productDetails.SeName;
                        model.ProductImageUrl = productDetails.DefaultPictureModel.ImageUrl;
                        model.productPrice = productDetails.ProductPrice.PriceValue;
                        model.CurrencyCode = productDetails.ProductPrice.CurrencyCode;
                        model.ProductAvailable = 1;
                    }
                }
            }
 
            return View("~/Plugins/Widgets.FollowPrice/Views/PublicInfo.cshtml", model);
        }
    }

13. Create a view component view file under ‘Views’ folder and add the Razor code.

14. For all the files under ‘Views’ folder and ‘plugin.json’ file right click and select properties. Select ‘Build Action’ as ‘Content’ and ‘Copy to Output Directory’ as ‘Copy if newer’ as shown in the screenshot below.



15. Build your project, test for the functionality, create a package and share with others to use.

We hope this article has given you insights into creating a widget-based nopCommerce plugin for nopCommerce version 4.0. Plugins help extend the functionality of your nopCommerce store and can be easily incorporated for various applications, such as like live chat, payment methods, shipping methods, etc.

If you would like to know more about plugin development in nopCommerce 4.0, please get in touch with us.

Manjunath Govindappa | ASP.NET Technical Lead

Leave A Comment

Aix-en-Provence – Bangalore – Montreal

We love diversity and we work out of 3 fabulous cities, on 3 continents, and 3 time zones.


Contact us
We’ll get in touch with you ASAP!