Views & ViewComponents
Learn how to organize views and create ViewComponents in your nopCommerce plugins.
View Organization
Using ViewLocationExpander (Recommended)
Instead of specifying explicit view paths in controllers, use ViewLocationExpander to automatically locate views. This is the recommended approach for plugin view management.
ViewLocationExpander.cs
// Infrastructure/ViewLocationExpander.cs
using Microsoft.AspNetCore.Mvc.Razor;
using Nop.Core.Infrastructure;
using Nop.Web.Framework;
using Nop.Web.Framework.Themes;
namespace NopStation.Plugin.Misc.PluginTemplate.Infrastructure;
public class ViewLocationExpander : IViewLocationExpander
{
protected const string THEME_KEY = "nop.themename";
public IEnumerable<string> ExpandViewLocations(
ViewLocationExpanderContext context,
IEnumerable<string> viewLocations)
{
// Add plugin-specific view locations
return new[]
{
$"~/Plugins/NopStation.Plugin.Misc.PluginTemplate/Areas/Admin/Views/Shared/{{0}}.cshtml",
$"~/Plugins/NopStation.Plugin.Misc.PluginTemplate/Areas/Admin/Views/{{1}}/{{0}}.cshtml"
}.Concat(viewLocations);
}
public void PopulateValues(ViewLocationExpanderContext context)
{
// Skip theme resolution for admin area (admin is not themeable)
if (context.AreaName?.Equals(AreaNames.ADMIN) ?? false)
return;
// Resolve theme for public views
context.Values[THEME_KEY] = EngineContext.Current
.Resolve<IThemeContext>()
.GetWorkingThemeNameAsync().Result;
}
}Register in NopStartup.cs
// Infrastructure/NopStartup.cs
using Microsoft.AspNetCore.Mvc.Razor;
public class NopStartup : INopStartup
{
public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
// Register ViewLocationExpander
services.Configure<RazorViewEngineOptions>(options =>
{
options.ViewLocationExpanders.Add(new ViewLocationExpander());
});
}
public void Configure(IApplicationBuilder application) { }
public int Order => 100;
}Benefits
With ViewLocationExpander registered, you can return views by name only:
// ❌ Without ViewLocationExpander (explicit paths)
return View("~/Plugins/NopStation.Plugin.Misc.PluginTemplate/Areas/Admin/Views/Configure.cshtml", model);
// ✅ With ViewLocationExpander (automatic discovery)
return View("Configure", model);
// Or simply:
return View(model); // Uses action name as view nameView Location Patterns
The placeholders in view paths work as follows:
{0}- View name (e.g., "Configure", "Index"){1}- Controller name (e.g., "PluginName", "Settings")
Manual View Paths (Alternative)
If you prefer explicit control, reference views with full paths:
// In controller
return View("~/Plugins/Widgets.MyAwesome/Views/Configure.cshtml", model);ViewComponents
ViewComponents are used to render widget content in nopCommerce. They inherit from NopViewComponent and integrate with the widget zone system.
ViewComponent Structure
Components/
└── CheckoutMessageViewComponent.cs # ViewComponent class
Views/
└── Shared/
└── Components/
└── CheckoutMessage/ # Named after component (minus "ViewComponent")
└── Default.cshtml # Default viewCreating a ViewComponent
// Components/CheckoutMessageViewComponent.cs
using Microsoft.AspNetCore.Mvc;
using Nop.Core;
using Nop.Services.Orders;
using Nop.Web.Framework.Components;
using Nop.Web.Models.ShoppingCart;
namespace NopStation.Plugin.Misc.CheckoutMessage.Components;
public class CheckoutMessageViewComponent : NopViewComponent
{
#region Fields
private readonly ICheckoutMessageService _checkoutMessageService;
private readonly IShoppingCartService _shoppingCartService;
private readonly IStoreContext _storeContext;
private readonly IWorkContext _workContext;
#endregion
#region Ctor
public CheckoutMessageViewComponent(
ICheckoutMessageService checkoutMessageService,
IShoppingCartService shoppingCartService,
IStoreContext storeContext,
IWorkContext workContext)
{
_checkoutMessageService = checkoutMessageService;
_shoppingCartService = shoppingCartService;
_storeContext = storeContext;
_workContext = workContext;
}
#endregion
#region Methods
public async Task<IViewComponentResult> InvokeAsync(string widgetZone, object additionalData)
{
// Validate additional data passed from widget zone
if (!(additionalData is ShoppingCartModel shoppingCartModel))
return Content("");
var customer = await _workContext.GetCurrentCustomerAsync();
var store = await _storeContext.GetCurrentStoreAsync();
var cart = await _shoppingCartService.GetShoppingCartAsync(
customer,
ShoppingCartType.ShoppingCart,
store.Id);
var model = await _checkoutMessageService.GetCheckoutMessageAsync(customer, cart);
// Return empty content if not visible
if (!model.IsVisible)
return Content("");
// Uses Views/Shared/Components/CheckoutMessage/Default.cshtml
return View(model);
}
#endregion
}ViewComponent View Location
Views are automatically located based on the component name:
| Component Class | View Path |
|---|---|
CheckoutMessageViewComponent | Views/Shared/Components/CheckoutMessage/Default.cshtml |
CustomWidgetViewComponent | Views/Shared/Components/CustomWidget/Default.cshtml |
Naming Convention
Remove the ViewComponent suffix from the class name to get the view folder name.
Registering with Widget Zones
In your main Plugin.cs, implement IWidgetPlugin:
public class Plugin : BasePlugin, IWidgetPlugin
{
public bool HideInWidgetList => false;
public Task<IList<string>> GetWidgetZonesAsync()
{
return Task.FromResult<IList<string>>(new List<string>
{
PublicWidgetZones.OrderSummaryContentBefore,
PublicWidgetZones.CheckoutConfirmTop
});
}
public Type GetWidgetViewComponent(string widgetZone)
{
return typeof(CheckoutMessageViewComponent);
}
}View Files Required
Ensure these files exist in Views/:
Views/
├── _ViewImports.cshtml # Tag helpers and namespaces
├── _ViewStart.cshtml # Layout configuration (optional)
└── Shared/
└── Components/
└── {ComponentName}/ # Without "ViewComponent" suffix
└── Default.cshtml # Default view nameSample Default.cshtml
@model CheckoutMessageModel
@if (Model.IsVisible)
{
<div class="checkout-message">
<p>@Model.Message</p>
</div>
}Next Steps
- Admin UI - Build admin interfaces
- Frontend Integration - Public store features
- Events - React to system events