Sample: Mega menu

Overview

This sample demonstrates how to build a custom navigation menu with nested child widgets. It is useful for large sites where the sitemap does not fit into one or two levels. Additionally, you can use the sample to insert custom widgets inside the menu and display them when hovering over a specific page.

You can choose from the following options:

PREREQUISITES: You must set up a Sitefinity renderer application and connect it to your Sitefinity CMS application. For more information, see Install Sitefinity in ASP.NET Core mode.

NOTE: The instructions in this sample use Visual Studio 2022 and a Sitefinity renderer project named Renderer.

Folder structure

Under your Renderer project, you must create the following folders:

  • Entities/MegaMenu
  • Models/MegaMenu
  • ViewModels/MegaMenu
  • ViewComponents
  • Views/Shared/Components/MegaMenu
  • wwwroot/styles

Create a custom layout file

You must register the scripts in the _Layout.cshtml file. You must then use this custom base layout for every page where the MegaMenu is placed

Perform the following:

  1. In the context menu of folder Views/Shared, click Add » Class…
  2. Select Code File.
  3. In Name, enter _Layout.cshtml and click Add.
  4. In the file, paste the following code and save your changes:

    @model Progress.Sitefinity.AspNetCore.Models.PageModel
    @using Progress.Sitefinity.AspNetCore.Mvc.Rendering
    @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
    @addTagHelper *, Progress.Sitefinity.AspNetCore
    @inject Progress.Sitefinity.AspNetCore.Web.IRequestContext requestContext;
    @inject Progress.Sitefinity.AspNetCore.Web.IRenderContext renderContext;
    <!DOCTYPE html>
    <html id="html" lang="@requestContext.Culture">
    <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="icon" type="image/x-icon" href="favicon.ico">
    @Html.RenderSeoMeta(this.Model)
    <environment include="Development">
    <link rel="stylesheet" href="@Progress.Sitefinity.AspNetCore.Constants.PrefixRendererUrl("Styles/bootstrap.css")" />
    </environment>
    <environment exclude="Development">
    <link rel="stylesheet" href="@Progress.Sitefinity.AspNetCore.Constants.PrefixRendererUrl("Styles/bootstrap.min.css")" />
    </environment>
    @* Scripts for the OOB Sitefinity widgets *@
    <link rel="stylesheet" href="@Progress.Sitefinity.AspNetCore.Constants.PrefixRendererUrl("styles/main.css")" type="text/css" />
    @* Custom styles *@
    <link rel="stylesheet" href="~/styles/styles.css" />
    @if (this.renderContext.IsEdit)
    {
    @* Custom styles loaded only in Edit mode *@
    <link rel="stylesheet" href="~/styles/edit.css" />
    }
    @* Custom scripts *@
    <script src="~/scripts/scripts.js"></script>
    </head>
    <body class="container-fluid">
    @RenderSection("Scripts")
    </body>
    </html>
    view raw _Layout.cshtml hosted with ❤ by GitHub

Create the widget

  • In the context menu of folder Entities/MegaMenu, click Add » Class…
  • In Name, enter MegaMenuEntity.cs and click Add.
  • In the class, paste the following code and save your changes:

using System.Collections.Generic;
using System.ComponentModel;
using Progress.Sitefinity.AspNetCore.Widgets.Models.Common;
using Progress.Sitefinity.AspNetCore.Widgets.Models.Navigation;
using Progress.Sitefinity.AspNetCore.Widgets.Models.Section;
using Progress.Sitefinity.Renderer.Designers;
using Progress.Sitefinity.Renderer.Designers.Attributes;
using Progress.Sitefinity.Renderer.Entities.Content;
using Progress.Sitefinity.RestSdk;
namespace Renderer.Entities.MegaMenu
{
public class MegaMenuEntity : NavigationEntity
{
[ContentSection("First Section", 0)]
[DisplayName("First page")]
[Content(Type = KnownContentTypes.Pages, AllowMultipleItemsSelection = false)]
public MixedContentContext FirstPage { get; set; }
[ContentSection("First Section", 1)]
[DisplayName("First section css")]
public string FirstSectionCss { get; set; }
[ContentSection("First Section", 2)]
[DisplayName("First section proportions")]
public IList<string> FirstSectionProportions { get; set; }
[ContentSection("Second Section", 0)]
[DisplayName("Second page")]
[Content(Type = KnownContentTypes.Pages, AllowMultipleItemsSelection = false)]
public MixedContentContext SecondPage { get; set; }
[ContentSection("Second Section", 1)]
[DisplayName("Second section css")]
public string SecondSectionCss { get; set; }
[ContentSection("Second Section", 2)]
[DisplayName("Second section proportions")]
public IList<string> SecondSectionProportions { get; set; }
[ContentSection("Third Section", 0)]
[DisplayName("Third page")]
[Content(Type = KnownContentTypes.Pages, AllowMultipleItemsSelection = false)]
public MixedContentContext ThirdPage { get; set; }
[ContentSection("Third Section", 1)]
[DisplayName("Third section css")]
public string ThirdSectionCss { get; set; }
[ContentSection("Third Section", 2)]
[DisplayName("Third section proportions")]
public IList<string> ThirdSectionProportions { get; set; }
public bool HideSectionsInEdit { get; set; }
}
}

  • In the context menu of folder Models/MegaMenu, click Add » Class…
  • In Name, enter MegaMenuModel.cs and click Add.
  • In the class, paste the following code and save your changes:

using System.Collections.Generic;
using System.Threading.Tasks;
using Progress.Sitefinity.RestSdk;
using Progress.Sitefinity.RestSdk.Client;
using Progress.Sitefinity.RestSdk.Client.Dto;
using Progress.Sitefinity.RestSdk.Clients.Pages.Dto;
using Progress.Sitefinity.AspNetCore.Web;
using Progress.Sitefinity.AspNetCore.Widgets.Models.Navigation;
using Progress.Sitefinity.AspNetCore.Widgets.Models.Section;
using Renderer.Entities.MegaMenu;
using Renderer.ViewModels.MegaMenu;
using System;
using Progress.Sitefinity.AspNetCore.RestSdk;
namespace Renderer.Models.MegaMenu
{
public class MegaMenuModel : IMegaMenuModel
{
private IRestClient restClient;
private INavigationModel navigationModel;
public MegaMenuModel(IRestClient restClient, INavigationModel navigationModel)
{
this.restClient = restClient;
this.navigationModel = navigationModel;
}
public async Task<MegaMenuViewModel> InitializeViewModel(MegaMenuEntity entity, IRenderContext renderContext)
{
var viewModel = new MegaMenuViewModel();
viewModel.HideSectionsInEdit = entity.HideSectionsInEdit;
viewModel.FirstSectionCss = entity.FirstSectionCss;
viewModel.FirstSectionProportions = entity.FirstSectionProportions ?? new List<string>();
viewModel.SecondSectionCss = entity.SecondSectionCss;
viewModel.SecondSectionProportions = entity.SecondSectionProportions ?? new List<string>();
viewModel.ThirdSectionCss = entity.ThirdSectionCss;
viewModel.ThirdSectionProportions = entity.ThirdSectionProportions ?? new List<string>();
var allContexts = new[] { entity.FirstPage, entity.SecondPage, entity.ThirdPage };
var allPagesResponse = await this.restClient.GetItems<PageNodeDto>(allContexts, new GetAllArgs()).ConfigureAwait(true);
if (allPagesResponse.Items.Count > 0)
viewModel.FirstPageId = allPagesResponse.Items[0].Id;
else
viewModel.FirstPageId = Guid.Empty.ToString();
if (allPagesResponse.Items.Count > 1)
viewModel.SecondPageId = allPagesResponse.Items[1].Id;
else
viewModel.SecondPageId = Guid.Empty.ToString();
if (allPagesResponse.Items.Count > 2)
viewModel.ThirdPageId = allPagesResponse.Items[2].Id;
else
viewModel.ThirdPageId = Guid.Empty.ToString();
viewModel.NavigationViewModel = await this.navigationModel.InitializeViewModel(entity).ConfigureAwait(true);
return viewModel;
}
}
}

  • In the context menu of folder Models/MegaMenu, click Add » Class…
  • In Name, enter IMegaMenuModel.cs and click Add.
  • In the class, paste the following code and save your changes:

using System.Threading.Tasks;
using Progress.Sitefinity.AspNetCore.Web;
using Renderer.Entities.MegaMenu;
using Renderer.ViewModels.MegaMenu;
namespace Renderer.Models.MegaMenu
{
public interface IMegaMenuModel
{
Task<MegaMenuViewModel> InitializeViewModel(MegaMenuEntity entity, IRenderContext renderContext);
}
}

  • In the context menu of folder ViewModels/MegaMenu, click Add » Class…
  • In Name, enter MegaMenuViewModel.cs and click Add.
  • In the class, paste the following code and save your changes:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Progress.Sitefinity.AspNetCore.ViewComponents;
using Progress.Sitefinity.AspNetCore.Widgets.Models.Navigation;
using Progress.Sitefinity.AspNetCore.Widgets.Models.Section;
using Renderer.Entities.MegaMenu;
namespace Renderer.ViewModels.MegaMenu
{
public class MegaMenuViewModel
{
public string FirstPageId { get; set; }
public string FirstSectionCss { get; set; }
public IList<string> FirstSectionProportions { get; set; }
public string SecondPageId { get; set; }
public string SecondSectionCss { get; set; }
public IList<string> SecondSectionProportions { get; set; }
public string ThirdPageId { get; set; }
public string ThirdSectionCss { get; set; }
public IList<string> ThirdSectionProportions { get; set; }
public bool HideSectionsInEdit { get; set; }
public NavigationViewModel NavigationViewModel { get; set; }
public ICompositeViewComponentContext<MegaMenuEntity> Context { get; set; }
}
}

  • In the context menu of folder ViewComponents, click Add » Class…
  • In Name, enter MegaMenuViewComponent.cs and click Add.
  • In the class, paste the following code and save your changes:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Progress.Sitefinity.AspNetCore.ViewComponents;
using Progress.Sitefinity.AspNetCore.Web;
using Renderer.Entities.MegaMenu;
using Renderer.Models.MegaMenu;
namespace Renderer.ViewComponents
{
[SitefinityWidget(Title = "Mega Menu", Category = WidgetCategory.Content, Section = WidgetSection.NavigationAndSearch, EmptyIconText = "No pages have been published", EmptyIconAction = EmptyLinkAction.None, EmptyIcon = "file-text-o")]
public class MegaMenuViewComponent : ViewComponent
{
private IMegaMenuModel megaMenuModel;
private IRenderContext renderContext;
public MegaMenuViewComponent(IMegaMenuModel megaMenuModel, IRenderContext renderContext)
{
this.megaMenuModel = megaMenuModel;
this.renderContext = renderContext;
}
public async Task<IViewComponentResult> InvokeAsync(ICompositeViewComponentContext<MegaMenuEntity> context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
var viewModel = await this.megaMenuModel.InitializeViewModel(context.Entity, this.renderContext);
viewModel.Context = context;
return this.View(viewModel);
}
}
}

  • In the context menu of folder Views/Shared/Components/MegaMenu, click Add » Class…
  • Select Code File.
  • In Name, enter Default.cshtml and click Add.
  • In the class, paste the following code and save your changes:

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Progress.Sitefinity.AspNetCore
@using Progress.Sitefinity.AspNetCore.ViewComponents;
@using Progress.Sitefinity.AspNetCore.Mvc.Rendering;
@using Progress.Sitefinity.AspNetCore.Web;
@using Progress.Sitefinity.AspNetCore.Widgets.Models.Navigation;
@using Renderer.ViewModels.MegaMenu;
@inject IRenderContext renderContext;
@model MegaMenuViewModel
<environment include="Development">
<script src="Scripts/bootstrap.bundle.js" section-name="Top" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"></script>
</environment>
<environment exclude="Development">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js"
section-name="Top"
asp-fallback-href="Scripts/bootstrap.bundle.min.js"
integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"></script>
</environment>
<div class="@Model.NavigationViewModel.WrapperCssClass mega-menu">
<nav class="navbar navbar-expand-md navbar-light">
<div class="container-fluid">
<button class="navbar-toggler ms-auto" type="button" data-bs-toggle="collapse" data-bs-target='@Html.GetUniqueId("#navbar")' aria-controls='@Html.GetUniqueId("navbar")' aria-expanded="false" aria-label="Toggle Navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="@Html.GetUniqueId("navbar")">
<ul class="navbar-nav mb-2 mb-md-0 flex-wrap">
@foreach (var node in @Model.NavigationViewModel.Nodes)
{
await RenderRootLevelNode(node);
}
</ul>
</div>
</div>
</nav>
@if (this.renderContext.IsEdit)
{
await RenderSections(null);
}
</div>
@*Here is specified the rendering for the root level*@
@{ async Task RenderRootLevelNode(PageViewModel node)
{
var hasSection = await HasSection(node);
if (hasSection || node.ChildNodes.Count > 0)
{
<li class="nav-item me-md-3 dropdown @GetClass(node)">
<a class="nav-link dropdown-toggle" href="#" id='@Html.GetUniqueId("navbarDropdownMenuLink" + node?.Key)' data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">@node.Title</a>
@{
if (!hasSection)
{
<ul class="dropdown-menu" aria-labelledby='@Html.GetUniqueId("navbarDropdownMenuLink" + node?.Key)'>
@{ await RenderSubLevelsRecursive(node); }
</ul>
}
else
{
await RenderSections(node);
}
}
</li>
}
else
{
<li class="nav-item">
<a class="nav-link @GetClass(node)" href="@node.Url" target="@node.LinkTarget">@node.Title</a>
@{ await RenderSections(node); }
</li>
}
}
}
@*Here is specified the rendering for all child levels*@
@{ async Task RenderSubLevelsRecursive(PageViewModel node)
{
foreach (var childNode in node.ChildNodes)
{
if (childNode.ChildNodes.Count > 0)
{
<li class="dropdown-submenu nav-item">
<a class="dropdown-item nav-link @GetClass(childNode)" href="@childNode.Url" target="@childNode.LinkTarget">
@childNode.Title
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-caret-right-fill" viewBox="0 0 16 16">
<path d="M12.14 8.753l-5.482 4.796c-.646.566-1.658.106-1.658-.753V3.204a1 1 0 0 1 1.659-.753l5.48 4.796a1 1 0 0 1 0 1.506z" />
</svg>
</a>
<ul class="dropdown-menu">
@{ await RenderSubLevelsRecursive(childNode); }
</ul>
@{ await RenderSections(node); }
</li>
}
else
{
<li class="nav-item">
<a class="dropdown-item nav-link @GetClass(childNode)" href="@childNode.Url" target="@childNode.LinkTarget">@childNode.Title</a>
@{ await RenderSections(node); }
</li>
}
}
}
}
@*Resolves the class that will be added for each node depending whether it is selected*@
@{Microsoft.AspNetCore.Html.IHtmlContent GetClass(PageViewModel node)
{
if (node.IsCurrentlyOpened)
{
return Html.HtmlSanitize("active");
}
return null;
}
}
@{ async Task<bool> RenderSections(PageViewModel node)
{
// do not render the sections in edit mode this way
if (node != null && this.renderContext.IsEdit)
return false;
var style = string.Empty;
var renderedSection = false;
if (this.Model.HideSectionsInEdit)
style = "display: none !important";
@if (node == null || node?.Key.ToUpperInvariant() == this.Model.FirstPageId.ToUpperInvariant())
{
@if (this.Model.FirstSectionProportions.Count > 0)
{
<section class="row mega-menu__item dropdown-menu @this.Model.FirstSectionCss" aria-labelledby='@Html.GetUniqueId("navbarDropdownMenuLink" + node?.Key)' style="@style">
@for (var i = 0; i < this.Model.FirstSectionProportions.Count; i++)
{
<div class="col-md-@this.Model.FirstSectionProportions[i]" data-sfcontainer container-context="@this.Model.Context.ContainerContext("Column1" + i, "Column 1." + i)">
</div>
}
</section>
renderedSection = true;
}
}
@if (node == null || node?.Key.ToUpperInvariant() == this.Model.SecondPageId.ToUpperInvariant())
{
@if (this.Model.SecondSectionProportions.Count > 0)
{
<section class="row @this.Model.SecondSectionCss mega-menu__item dropdown-menu" aria-labelledby='@Html.GetUniqueId("navbarDropdownMenuLink" + node?.Key)' style="@style">
@for (var i = 0; i < this.Model.SecondSectionProportions.Count; i++)
{
<div class="col-md-@this.Model.SecondSectionProportions[i]" data-sfcontainer container-context="@this.Model.Context.ContainerContext("Column2" + i, "Column 2." + i)">
</div>
}
</section>
renderedSection = true;
}
}
@if (node == null || node?.Key.ToUpperInvariant() == this.Model.ThirdPageId.ToUpperInvariant())
{
@if (this.Model.ThirdSectionProportions.Count > 0)
{
<section class="row @this.Model.ThirdSectionCss mega-menu__item dropdown-menu" aria-labelledby='@Html.GetUniqueId("navbarDropdownMenuLink" + node?.Key)' style="@style">
@for (var i = 0; i < this.Model.ThirdSectionProportions.Count; i++)
{
<div class="col-md-@this.Model.ThirdSectionProportions[i]" data-sfcontainer container-context="@this.Model.Context.ContainerContext("Column3" + i, "Column 3." + i)">
</div>
}
</section>
renderedSection = true;
}
}
return renderedSection;
}
}
@{ async Task<bool> HasSection(PageViewModel node)
{
if (node == null || this.renderContext.IsEdit)
return false;
@if (node.Key.ToUpperInvariant() == this.Model.FirstPageId.ToUpperInvariant() && this.Model.FirstSectionProportions.Count > 0)
{
return true;
}
@if (node.Key.ToUpperInvariant() == this.Model.SecondPageId.ToUpperInvariant() && this.Model.SecondSectionProportions.Count > 0)
{
return true;
}
@if (node.Key.ToUpperInvariant() == this.Model.ThirdPageId.ToUpperInvariant() && this.Model.ThirdSectionProportions.Count > 0)
{
return true;
}
return false;
}
}
view raw Default.cshtml hosted with ❤ by GitHub

Import the common directives

The Program.cs file should look in the following way:

  • In the context menu of folder Views, click Add » Class…
  • Select Code File.
  • In Name, enter _LayoutImports.cshtml and click Add.
  • In the class, paste the following code and save your changes:
    @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
    @addTagHelper *, Progress.Sitefinity.AspNetCore
    @using Progress.Sitefinity.AspNetCore.ViewComponents;
  • In the context menu of folder Views/Shared/Components, click Add » Class…
  • Select Code File.
  • In Name, enter _LayoutImports.cshtml and click Add.
  • In the class, paste the following code and save your changes:
    @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
    @addTagHelper *, Progress.Sitefinity.AspNetCore
    @using Progress.Sitefinity.AspNetCore.ViewComponents;
    @using Progress.Sitefinity.AspNetCore.Mvc.Rendering;

Create the syles

  • In the context menu of folder wwwroot/styles, click Add » New Item…
  • In Name, enter styles.css and click Add.
  • In the file, paste the following code and save your changes:

.mega-menu .mega-menu__item {
top: 40px;
width: 50vw;
padding: 20px;
background: #fff;
border-radius: 5px;
box-shadow: 0px 2px 15px 2px rgba(0,0,0,0.75);
}
.mega-menu .nav-item {
position: relative;
}
.mega-menu .nav-item .mega-menu__item {
position: absolute;
visibility: hidden;
opacity: 0;
transition: opacity 0.1s ease;
}
.mega-menu .nav-item .mega-menu__item.show {
visibility: visible;
opacity: 1;
display: flex;
}
view raw styles.css hosted with ❤ by GitHub

  • In the context menu of folder wwwroot/styles, click Add » New Item…
  • In Name, enter edit.css and click Add.
  • In the file, paste the following code and save your changes:

.mega-menu .mega-menu__item {
position: relative !important;
top: 0 !important;
margin-bottom: 30px !important;
display: flex !important;
}
view raw edit.css hosted with ❤ by GitHub

Build your solution.

Result

When you open your Renderer application and open the New editor, you will see the MegaMenu widget in the widget selector. When you add the widget on your page and edit it, you can choose which pages to be part of the menu and how to display them.

MegaMenu

Run the sample

This sample is available in Sitefinity’s GitHub repository. You can run and play with it.
To do this, perform the following:

  1. Go to Sitefinity’s GitHub repository Sitefinity ASP.NET Core samples.
  2. Expand Code and click Download ZIP.
  3. Extract the files on your computer.
  4. In the extracted folder, navigate to sitefinity-aspnetcore-mvc-samples-master/src/mega-menu folder.
  5. Open the mega-menu.sln in Visual Studio.
  6. Open the appsettings.json file.
  7. In section “Sitefinity”, change the “Url” property to the URL of your Sitefinity CMS site.
    If you have deployed Sitefinity CMS on the IIS, point to “https://localhost:<https_port>".
  8. In Visual Studio, in the context menu of mega-menu project, click View in Browser.
  9. Log in to your Sitefinity CMS instance and place the widget on a page.

Want to learn more?

Increase your Sitefinity skills by signing up for our free trainings. Get Sitefinity-certified at Progress Education Community to boost your credentials.

Get started with Integration Hub | Sitefinity Cloud | Sitefinity SaaS

This free lesson teaches administrators, marketers, and other business professionals how to use the Integration hub service to create automated workflows between Sitefinity and other business systems.

Web Security for Sitefinity Administrators

This free lesson teaches administrators the basics about protecting yor Sitefinity instance and its sites from external threats. Configure HTTPS, SSL, allow lists for trusted sites, and cookie security, among others.

Foundations of Sitefinity ASP.NET Core Development

The free on-demand video course teaches developers how to use Sitefinity .NET Core and leverage its decoupled architecture and new way of coding against the platform.

Was this article helpful?

Next article

Sample: Tabs section