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
- Frontend Integration - Public store UI
- Localization - Translate your admin UI
- Configuration - Advanced settings