Skip to content

Admin UI Integration

Build rich administration interfaces for your nopCommerce plugins.

Overview

The nopCommerce admin panel uses:

  • Razor Views for rendering
  • DataTables for grids
  • jQuery Validation for forms
  • AdminLTE styling framework

Creating an Admin Controller

csharp
// Controllers/MyPluginController.cs
using Microsoft.AspNetCore.Mvc;
using Nop.Web.Framework;
using Nop.Web.Framework.Controllers;
using Nop.Web.Framework.Mvc.Filters;

namespace Nop.Plugin.Widgets.MyPlugin.Controllers;

[AuthorizeAdmin]
[Area(AreaNames.ADMIN)]
[AutoValidateAntiforgeryToken]
public class MyPluginController : BasePluginController
{
    #region Fields

    private readonly IMyService _myService;
    private readonly ISettingService _settingService;
    private readonly INotificationService _notificationService;

    #endregion

    #region Ctor

    public MyPluginController(
        IMyService myService,
        ISettingService settingService,
        INotificationService notificationService)
    {
        _myService = myService;
        _settingService = settingService;
        _notificationService = notificationService;
    }

    #endregion

    #region Methods

    public async Task<IActionResult> Configure()
    {
        var settings = await _settingService.LoadSettingAsync<MyPluginSettings>();
        
        var model = new ConfigurationModel
        {
            Enabled = settings.Enabled,
            ApiKey = settings.ApiKey,
            MaxItems = settings.MaxItems
        };
        
        return View("~/Plugins/Widgets.MyPlugin/Views/Configure.cshtml", model);
    }

    [HttpPost]
    public async Task<IActionResult> Configure(ConfigurationModel model)
    {
        if (!ModelState.IsValid)
            return await Configure();

        var settings = await _settingService.LoadSettingAsync<MyPluginSettings>();
        
        settings.Enabled = model.Enabled;
        settings.ApiKey = model.ApiKey;
        settings.MaxItems = model.MaxItems;
        
        await _settingService.SaveSettingAsync(settings);
        
        _notificationService.SuccessNotification("Configuration saved successfully");
        
        return await Configure();
    }

    #endregion
}

Configuration View

html
<!-- Views/Configure.cshtml -->
@model ConfigurationModel
@{
    Layout = "_ConfigurePlugin";
    ViewData["Title"] = "Configure My Plugin";
}

<form asp-controller="MyPlugin" asp-action="Configure" method="post">
    <div class="card card-default">
        <div class="card-header">
            <h5 class="card-title">General Settings</h5>
        </div>
        <div class="card-body">
            <div class="form-group row">
                <div class="col-md-3">
                    <label asp-for="Enabled" class="col-form-label"></label>
                </div>
                <div class="col-md-9">
                    <input asp-for="Enabled" class="form-check-input" />
                    <span asp-validation-for="Enabled"></span>
                </div>
            </div>
            
            <div class="form-group row">
                <div class="col-md-3">
                    <label asp-for="ApiKey" class="col-form-label"></label>
                </div>
                <div class="col-md-9">
                    <input asp-for="ApiKey" class="form-control" />
                    <span asp-validation-for="ApiKey"></span>
                </div>
            </div>
            
            <div class="form-group row">
                <div class="col-md-3">
                    <label asp-for="MaxItems" class="col-form-label"></label>
                </div>
                <div class="col-md-9">
                    <input asp-for="MaxItems" class="form-control" type="number" />
                    <span asp-validation-for="MaxItems"></span>
                </div>
            </div>
        </div>
        <div class="card-footer">
            <button type="submit" class="btn btn-primary">
                <i class="far fa-save"></i> Save
            </button>
        </div>
    </div>
</form>

Creating DataTables Grid

Model

csharp
// Models/RecordListModel.cs
public class RecordListModel
{
    public RecordSearchModel SearchModel { get; set; }
    public IList<RecordModel> Data { get; set; }
}

public class RecordSearchModel
{
    public string SearchName { get; set; }
    public bool? SearchActive { get; set; }
}

public class RecordModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsActive { get; set; }
    public DateTime CreatedOn { get; set; }
}

Controller Actions

csharp
public IActionResult List()
{
    var model = new RecordSearchModel();
    return View("~/Plugins/Widgets.MyPlugin/Views/List.cshtml", model);
}

[HttpPost]
public async Task<IActionResult> ListData(RecordSearchModel searchModel)
{
    var records = await _myService.GetPagedRecordsAsync(
        searchName: searchModel.SearchName,
        isActive: searchModel.SearchActive,
        pageIndex: searchModel.Page - 1,
        pageSize: searchModel.PageSize);

    var model = new DataTablesModel<RecordModel>
    {
        Data = records.Select(r => new RecordModel
        {
            Id = r.Id,
            Name = r.Name,
            IsActive = r.IsActive,
            CreatedOn = r.CreatedOnUtc
        }).ToList(),
        Total = records.TotalCount,
        TotalFiltered = records.TotalCount
    };

    return Json(model);
}

List View with DataTables

html
<!-- Views/List.cshtml -->
@model RecordSearchModel
@{
    Layout = "_AdminLayout";
    ViewData["Title"] = "Records";
}

<div class="content-header">
    <h1>Records</h1>
</div>

<div class="content">
    <div class="card card-default">
        <div class="card-header">
            <a class="btn btn-primary" asp-action="Create">
                <i class="fas fa-plus"></i> Add New
            </a>
        </div>
        <div class="card-body">
            <!-- Search panel -->
            <div class="row mb-3">
                <div class="col-md-4">
                    <input asp-for="SearchName" class="form-control" placeholder="Search by name..." />
                </div>
                <div class="col-md-3">
                    <select asp-for="SearchActive" class="form-control">
                        <option value="">All</option>
                        <option value="true">Active</option>
                        <option value="false">Inactive</option>
                    </select>
                </div>
                <div class="col-md-2">
                    <button type="button" id="search-btn" class="btn btn-info">
                        <i class="fas fa-search"></i> Search
                    </button>
                </div>
            </div>

            <!-- DataTable -->
            <table id="records-grid" class="table table-bordered table-hover">
                <thead>
                    <tr>
                        <th>ID</th>
                        <th>Name</th>
                        <th>Active</th>
                        <th>Created</th>
                        <th>Actions</th>
                    </tr>
                </thead>
            </table>
        </div>
    </div>
</div>

@section scripts {
<script>
    $(document).ready(function() {
        var table = $('#records-grid').DataTable({
            processing: true,
            serverSide: true,
            ajax: {
                url: '@Url.Action("ListData")',
                type: 'POST',
                data: function(d) {
                    d.SearchName = $('#SearchName').val();
                    d.SearchActive = $('#SearchActive').val();
                }
            },
            columns: [
                { data: 'Id' },
                { data: 'Name' },
                { 
                    data: 'IsActive',
                    render: function(data) {
                        return data ? '<span class="badge bg-success">Yes</span>' 
                                    : '<span class="badge bg-secondary">No</span>';
                    }
                },
                { 
                    data: 'CreatedOn',
                    render: function(data) {
                        return new Date(data).toLocaleDateString();
                    }
                },
                {
                    data: 'Id',
                    render: function(data) {
                        return '<a href="/Admin/MyPlugin/Edit/' + data + '" class="btn btn-sm btn-primary">Edit</a> ' +
                               '<button onclick="deleteRecord(' + data + ')" class="btn btn-sm btn-danger">Delete</button>';
                    }
                }
            ]
        });

        $('#search-btn').on('click', function() {
            table.ajax.reload();
        });
    });
</script>
}

Adding Admin Menu Items

csharp
// Infrastructure/AdminMenuPlugin.cs
using Nop.Services.Plugins;
using Nop.Web.Framework.Menu;

public class AdminMenuPlugin : IAdminMenuPlugin
{
    public async Task ManageSiteMapAsync(SiteMapNode rootNode)
    {
        var menuItem = new SiteMapNode
        {
            Title = "My Plugin",
            SystemName = "MyPlugin",
            IconClass = "far fa-dot-circle",
            Visible = true,
            ChildNodes = new List<SiteMapNode>
            {
                new SiteMapNode
                {
                    Title = "Configuration",
                    SystemName = "MyPlugin.Configuration",
                    ControllerName = "MyPlugin",
                    ActionName = "Configure",
                    IconClass = "far fa-cog"
                },
                new SiteMapNode
                {
                    Title = "Records",
                    SystemName = "MyPlugin.Records",
                    ControllerName = "MyPlugin",
                    ActionName = "List",
                    IconClass = "far fa-list"
                }
            }
        };

        var pluginNode = rootNode.ChildNodes.FirstOrDefault(x => x.SystemName == "Third party plugins");
        pluginNode?.ChildNodes.Add(menuItem);
    }
}

Model Validation

csharp
// Validators/ConfigurationModelValidator.cs
using FluentValidation;

public class ConfigurationModelValidator : AbstractValidator<ConfigurationModel>
{
    public ConfigurationModelValidator()
    {
        RuleFor(x => x.ApiKey)
            .NotEmpty()
            .WithMessage("API Key is required")
            .When(x => x.Enabled);

        RuleFor(x => x.MaxItems)
            .InclusiveBetween(1, 100)
            .WithMessage("Max items must be between 1 and 100");
    }
}

Next Steps

Released under the nopCommerce Public License.