Exam Area: Area 4 – Framework Components (15%)
Reference: https://docs.developers.optimizely.com/content-management-system/docs/extending-the-cms-ui
using EPiServer.Shell.Navigation;
[ServiceConfiguration(typeof(IMenuProvider))]
public class SiteMenuProvider : IMenuProvider
{
public IEnumerable<MenuItem> GetMenuItems()
{
// Add to the Global CMS menu
var rootItem = new SectionMenuItem("My Company Tools",
MenuPaths.Global + "/mycompany");
var toolItem = new UrlMenuItem(
"Product Sync",
MenuPaths.Global + "/mycompany/product-sync",
"/episerver/mycompany/product-sync")
{
IsAvailable = request => request.User.IsInRole("ProductManagers"),
SortIndex = 100,
AuthorizationPolicy = CmsPolicy.ManageContent
};
yield return rootItem;
yield return toolItem;
}
}
// Built-in menu paths
MenuPaths.Global // Top-level
MenuPaths.Global + "/cms" // CMS menu
MenuPaths.Global + "/cms/edit" // Edit
MenuPaths.Global + "/cms/admin" // Admin
MenuPaths.Global + "/cms/reports" // Reports
[Authorize(Roles = "WebAdmins,ProductManagers")]
public class ProductSyncController : Controller
{
private readonly IProductSyncService _syncService;
public ProductSyncController(IProductSyncService syncService)
{
_syncService = syncService;
}
// GET /episerver/mycompany/product-sync
public IActionResult Index()
{
var model = new ProductSyncViewModel
{
LastSyncDate = _syncService.LastSyncDate,
ProductCount = _syncService.GetProductCount()
};
return View(model);
}
// POST
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Sync()
{
await _syncService.SyncAsync();
TempData["Success"] = "Sync completed!";
return RedirectToAction("Index");
}
}
@* Views/ProductSync/Index.cshtml *@
@model ProductSyncViewModel
@{
Layout = "~/Views/Shared/_CmsLayout.cshtml"; // Use CMS shell layout
}
<div class="epi-formArea">
<h1>Product Sync Tool</h1>
<dl>
<dt>Last sync</dt>
<dd>@Model.LastSyncDate?.ToString("dd MMM yyyy HH:mm")</dd>
<dt>Products</dt>
<dd>@Model.ProductCount</dd>
</dl>
@using (Html.BeginForm("Sync", "ProductSync", FormMethod.Post))
{
@Html.AntiForgeryToken()
<button type="submit" class="epi-cta">Sync Now</button>
}
</div>
// Legacy approach - CMS 11 style
[AdminPlugin(
"My Plugin",
Description = "A custom plugin",
SortIndex = 2000,
Area = PlugInArea.AdminMenu,
Url = "~/mycompany/my-plugin")]
public class MyPlugin { }
// Shell module view (Dojo-based)
[ServiceConfiguration(typeof(ViewConfiguration))]
public class MyCustomViewConfiguration : ViewConfiguration<MyContentType>
{
public MyCustomViewConfiguration()
{
Key = "my-custom-view";
Name = "My View";
Description = "Custom view description";
ControllerType = "myModule.Views.MyView"; // Dojo module path
IconClass = "epi-iconObjectsMedia";
}
}
// Reports in CMS Admin
[ServiceConfiguration(typeof(IReport))]
public class ContentAuditReport : IReport
{
public string Name => "Content Audit";
public string Description => "Audit content in the system";
public string Category => "Content";
public string Area => ReportArea.Cms;
public string ControllerTypeName => typeof(ContentAuditController).FullName;
}
[Authorize(Roles = "WebAdmins")]
public class ContentAuditController : Controller
{
public ActionResult Index()
{
return View(GetAuditData());
}
}
IMenuProvider interface used for? (Adding items to the CMS navigation menu)MenuPaths.Global + "/cms/admin" point? (The CMS Admin section in the menu)WebAdmins or custom roles)ViewConfiguration used for? (Registering custom views for content types)IReport interface used for? (Creating custom reports in CMS Admin)