DCMS - Dynamic Content and Menuing System
Dynamic Menus
This document explains how the proof-of-concept menu is loaded from the AllThingsPG database and displayed through the shared Razor partial view named _Menu.cshtml.
The menu is called a dynamic menu because its labels, hierarchy, visibility, order, and links come from database records. The application does not need a separate hard-coded <li> element for every menu entry. When the database records change, the menu can change the next time the application loads the records.
Overview: Why Use Dynamic Menus?
A static menu can provide reliable navigation, but a dynamic menu can provide a more personal and useful experience. AllThingsPG is intended to serve several audiences, including patients, caregivers, medical professionals, pharmaceutical representatives, and general visitors. Those audiences may need different information, different levels of detail, and different paths through the site.
The first step is to identify who is visiting. Once a visitor selects, or is otherwise associated with, a visitor type, the site can tailor both the navigation and the content shown to that audience. A patient may need practical information about living with the condition, a caregiver may need guidance on supporting someone else, and a medical professional may need more detailed clinical or diagnostic information. Pharmaceutical representatives may need a different set of resources again.
Each menu item can be associated with its own content. The same subject can therefore appear more than once in the database with the same visible Title, while each record points to a different audience, location in the menu, or content document. The VisitorMask determines which visitor types can see each item:
Current visitor: Medical professional
"Diagnosis" -> detailed clinical diagnosis content
Current visitor: Caregiver
"Diagnosis" -> caregiver-oriented explanation
Current visitor: Patient
"Diagnosis" -> patient-oriented explanation
The title may be the same because it is familiar and meaningful to every audience. The underlying MenuItemID, visitor mask, RouteTarget, and associated ContentDocument distinguish the records. This lets the application reuse clear navigation language without forcing every audience to receive the same material.
Dynamic menus therefore support two related goals:
- Enhanced user experience: visitors see navigation that is relevant to their role instead of a large, one-size-fits-all menu.
- Audience-specific content: each visible menu item can lead to content selected for that audience, including more specialized or detailed information where appropriate.
The menu is not the only part of this design, but it is the visible point where audience-aware navigation begins. The database stores the relationships, VisitorMask controls visibility, and the shared _Menu.cshtml partial view renders the appropriate result.
This approach also keeps the application flexible. Adding or changing audience-specific navigation and content can often be done by changing data and content records rather than rewriting the menu’s HTML structure.
The menu system has four main parts:
- The
MenuItemtable stores the menu records and RouteTarget values. PageBaseloads active menu records for a request._Layout.cshtmlprepares aMenuViewModeland calls_Menu.cshtml._Menu.cshtmlfilters, orders, and recursively renders the menu hierarchy.
1. What “Dynamic Menu” Means
A traditional static menu might contain the complete menu directly in a Razor file:
<ul>
<li><a href="/Visitor?V=Patient">Patient</a></li>
<li><a href="/Visitor?V=Caregiver">Caregiver</a></li>
</ul>
That approach requires a developer to edit the page whenever a menu item is added, removed, renamed, reordered, or redirected.
The proof-of-concept menu instead obtains the same information from MenuItem rows:
| MenuItem value | What it controls |
|---|---|
Title |
Text displayed to the visitor |
ParentMenuItemID |
Where the item appears in the hierarchy |
RouteTarget |
Route string followed or interpreted when the item is selected |
RouteType |
Application classification for the destination |
VisitorMask |
Visitor types allowed to see the item |
SortOrder |
Position among items with the same parent |
IsActive |
Whether the item is available to the menu |
The database supplies the menu data. Razor supplies the HTML structure.
2. The MenuItem Table
The runtime menu is stored in dbo.MenuItem.
| Column | Meaning |
|---|---|
MenuItemID |
Unique identifier for the menu record |
ParentMenuItemID |
The identifier of this item’s parent; NULL means top-level |
Title |
Label shown to the visitor |
RouteTarget |
Route string interpreted by the application |
RouteType |
One-character classification such as Visitor, Content, Page, or Redirect |
VisitorMask |
Concatenated visitor codes allowed to see the item |
SortOrder |
Relative order among items with the same parent |
IsActive |
Soft on/off switch for the menu item |
CreatedDate |
Creation timestamp |
ModifiedDate |
Last modification timestamp |
The self-reference through ParentMenuItemID creates the tree. A record with no parent is a top-level menu item. A record whose parent is item 8 is displayed underneath item 8.
Example hierarchy
Welcome
Patient
Caregiver
Provider
About PG
What is PG
Clinical Topics
Symptoms
Diagnosis
Treatment
The hierarchy is stored as IDs, not as indentation or hard-coded HTML.
3. Loading the Menu from the Database
Pages that use the shared layout inherit from PageBase. PageBase receives an AllThingsPgContext, which is the Entity Framework Core connection to the database.
Before a page handler runs, PageBase.OnPageHandlerExecuting prepares shared information:
public override void OnPageHandlerExecuting(PageHandlerExecutingContext context)
{
_ = VisitorTypes;
_ = RouteTypes;
MenuTree = BuildMenuTree();
base.OnPageHandlerExecuting(context);
}
The menu query is:
protected List<MenuItem> BuildMenuTree()
{
return _db.MenuItems
.AsNoTracking()
.Where(m => m.IsActive)
.OrderBy(m => m.SortOrder)
.ToList();
}
In plain language, the query:
- Reads from
MenuItem. - Uses
AsNoTracking()because the menu is being displayed, not edited in this request. - Keeps only records where
IsActiveis true. - Loads the records into
MenuTree. - Sorts the initial list by
SortOrder.
The parent-child relationships are not flattened into separate HTML rows by the database. The complete active set is loaded into memory, and _Menu.cshtml uses ParentMenuItemID to build the visible hierarchy.
4. Passing Menu Data to the Layout
The shared layout is:
Pages/Shared/_Layout.cshtml
The layout expects the current page model to be a PageBase, because it needs access to the shared menu and visitor information:
var model = (PageBase)ViewContext.ViewData.Model;
It then creates a MenuViewModel:
var menuViewModel = new MenuViewModel
{
MenuTree = model.MenuTree,
CurrentVisitor = model.CurrentVisitor,
RouteTypes = model.RouteTypes,
VisitorTypes = model.VisitorTypes
};
MenuViewModel is a small object that carries the information the partial needs:
| Property | Purpose |
|---|---|
MenuTree |
Active menu records loaded from the database |
CurrentVisitor |
The visitor persona currently selected |
RouteTypes |
Route-type lookup values available to the page |
VisitorTypes |
Visitor-persona lookup values used by the layout |
The layout displays the visitor-persona tabs and then calls the menu partial:
@await Html.PartialAsync("_Menu", menuViewModel)
This means the menu is part of the common page frame. Pages do not each need to repeat the menu markup.
5. The _Menu.cshtml Partial
The partial is located at:
Pages/Shared/_Menu.cshtml
It receives a MenuViewModel:
@model Proof_of_Concept.Models.MenuViewModel
The partial performs four jobs:
- Obtains the menu records.
- Finds the top-level records.
- Filters records for the current visitor.
- Recursively renders children and grandchildren.
The resulting HTML is an unordered list:
<ul class="pg-nav-links">
...
</ul>
The CSS classes control appearance. The partial controls which items exist and how they are nested.
6. Visitor Filtering
The current visitor is obtained by PageBase.CurrentVisitor. The selected visitor code is stored in session under PersonaCode. If there is no valid session value, PageBase falls back to the lookup row whose IsDefault value is true.
The menu extension method checks a visitor mask:
public bool IsVisibleTo(string visitorCode) =>
!string.IsNullOrEmpty(VisitorMask) &&
!string.IsNullOrEmpty(visitorCode) &&
VisitorMask.Contains(visitorCode);
The mask is a compact list of allowed visitor codes:
| VisitorMask | Meaning |
|---|---|
PCMDV |
Visible to all current visitor types |
P |
Visible to Patients |
PC |
Visible to Patients and Caregivers |
MD |
Visible to Providers and Pharmaceutical visitors |
For example, if the current visitor code is P, an item with mask PCMDV is visible and an item with mask C is not.
The current implementation uses string.Contains. Because visitor codes are single characters, each code is treated as one character in the mask.
7. Finding Top-Level Items
Top-level items have no parent:
GetTree()
.Where(m => m.ParentMenuItemId == null &&
m.IsVisibleTo(visitor!.Code))
.OrderBy(m => m.SortOrder)
Each qualifying record becomes an item in the main navigation bar.
The partial then checks whether the item has visible children:
var hasChildren = GetTree().Any(
m => m.ParentMenuItemId == item.MenuItemId &&
m.IsVisibleTo(visitor!.Code));
If children exist, the top-level item is displayed as a parent label:
<span class="pg-nav-parent">About PG</span>
If there are no children, it is displayed as a link:
<a href="/somewhere">Somewhere</a>
This allows a parent menu item to act as a category heading while child records provide the selectable destinations.
8. Rendering Child Menus Recursively
Child records are found by matching their ParentMenuItemID to the current parent’s MenuItemId:
var children = items
.Where(m => m.ParentMenuItemId == parentId &&
m.IsVisibleTo(visitor!.Code))
.OrderBy(m => m.SortOrder)
.ToList();
The partial renders each child as a link:
<ul class="pg-submenu">
@foreach (var child in children)
{
<li>
<a href="@child.GetUrl()">@child.Title</a>
@{ RenderChildren(items, child.MenuItemId); }
</li>
}
</ul>
The call to RenderChildren inside itself is recursive rendering. It means:
- Render this item’s children.
- For each child, look for grandchildren.
- Continue until an item has no children.
This is what allows the same code to display a menu with one level or several levels.
9. Building the Link
The database column is RouteTarget. It stores the route string used by the application; it is not a slug that the application must resolve by matching a title. For content routes, the target is the ContentDocumentID assigned to the placeholder document. MenuItemID and ContentDocumentID remain separate identities.
The supplied MenuItemList data includes these route forms:
| RouteType | RouteTarget example | Meaning |
|---|---|---|
| V | /visitor?target=Patient | Selects the Patient visitor experience. |
| C | /content?target=18 | Loads the content document with ID 18. |
| P | /page?target=clinicfinder.cshtml | Opens an application page. |
| R | /redirect?target=https://www.facebook.com/groups/368163159232 | Redirects to an external destination. |
MenuItemList exposes RouteTarget and the linked content fields; it does not calculate or synthesize the URL. The POC uses the route string from the menu record and adds menu context using the MenuItemID. Confirm the route handler whenever route behavior changes.
10. Caching the Menu
The partial uses IMemoryCache and a cache:
List<MenuItem> GetTree() =>
MemoryCache.GetOrCreate("MenuTree_" + visitor?.Code, entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);
return Model.MenuTree?.ToList() ?? new List<MenuItem>();
})!;
The cache key includes the visitor code:
MenuTree_P
MenuTree_C
MenuTree_V
The intended result is a separate cached menu set for each visitor persona, with a thirty-minute expiration. Caching reduces repeated preparation of the same menu data during that period.
The cache stores the MenuTree supplied by the current page model. The actual database query occurs in PageBase; the partial’s cache does not query the database by itself.
When menu records are changed, an already-cached menu can remain visible until its cache entry expires or is explicitly removed. A production implementation should decide how menu updates invalidate or refresh cached entries.
11. End-to-End Processing Flow
The complete current flow is:
Browser requests a Razor Page
|
v
PageModel is created
|
v
PageBase loads visitor and route lookup data
|
v
PageBase queries active MenuItem records
|
v
PageBase stores records in MenuTree
|
v
_Layout.cshtml receives the PageBase model
|
v
_Layout.cshtml creates MenuViewModel
|
v
_Layout.cshtml invokes _Menu.cshtml
|
v
_Menu.cshtml caches the supplied tree
|
v
_Menu.cshtml filters by VisitorMask
|
v
_Menu.cshtml orders by SortOrder
|
v
_Menu.cshtml recursively renders parent and child <ul>/<li> elements
|
v
Browser receives the completed page
Example: Patient visitor
- The visitor selects Patient.
- The visitor page resolves the Patient record.
- The selected code is stored in session.
PageBaseloads activeMenuItemrecords._Menu.cshtmluses visitor codeP.- Items whose
VisitorMaskcontainsPremain visible. - The remaining records are grouped by
ParentMenuItemID. - Each group is ordered by
SortOrder. - The partial writes the menu HTML.
- The visitor sees the Patient-appropriate navigation.
12. Responsibilities by File
| File | Responsibility |
|---|---|
Models/MenuItem.cs |
Entity Framework model representing a database menu row |
Models/MenuItem.Extensions.cs |
Returns the stored URL and evaluates visitor visibility |
Models/MenuViewModel.cs |
Carries menu, visitor, route, and persona data into the partial |
Models/AllThingsPGContext.cs |
Maps the application to the database and exposes MenuItems |
Pages/Page/PageBase.cs |
Loads lookup data, resolves the current visitor, and queries active menu rows |
Pages/Shared/_Layout.cshtml |
Builds the view model and invokes the shared menu partial |
Pages/Shared/_Menu.cshtml |
Filters, orders, nests, caches, and renders menu records |
wwwroot/css/site.css |
Site styling used by the rendered menu |
wwwroot/css/pg-components.css |
Component styling used by the rendered menu and page layout |
13. Current Design Boundaries
The current proof of concept intentionally keeps responsibilities separate:
| Responsibility | Current implementation |
|---|---|
| Store menu definitions | dbo.MenuItem |
| Load active menu records | PageBase.BuildMenuTree() |
| Remember selected visitor | ASP.NET Core session |
| Decide visitor visibility | MenuItem.IsVisibleTo() |
| Build the hierarchy | _Menu.cshtml and ParentMenuItemID |
| Order siblings | _Menu.cshtml and SortOrder |
| Produce the destination link | MenuItem.GetUrl() |
| Render common navigation on pages | _Layout.cshtml and _Menu.cshtml |
The partial does not create content, query content documents, or decide how a destination page processes its URL. It only displays the menu and emits the stored link.
14. Future Considerations
These are design considerations rather than claims about the current implementation:
- A centralized route service could build URLs from
RouteTypeand a separate route target. - Menu cache invalidation could refresh cached trees immediately after an administrative change.
- A dedicated menu service could move database loading out of
PageBase. - A view model could contain an already-built tree if the application needs more complex rendering rules.
- URL generation could use framework link helpers when route parameters need encoding or validation.
- Additional safeguards could detect cycles in parent-child data before recursive rendering.
These changes would extend the current approach; they are not required to understand how the current proof-of-concept menu works.
Current data status
The supplied MenuItemList extract confirms that the menu is populated and that each content-route row has its own linked placeholder document. Those placeholders are Draft and unpublished; authored PG articles have not yet been loaded. Repeated labels such as Emotional Journey have separate MenuItemID and ContentDocumentID values for different menu branches and visitor masks.
Summary
The dynamic menu is a database-backed navigation component. PageBase loads active MenuItem rows into MenuTree, _Layout.cshtml packages that data into a MenuViewModel, and _Menu.cshtml renders the visible hierarchy.
The partial uses the current visitor code to apply VisitorMask, uses ParentMenuItemID to find children, uses SortOrder to arrange siblings, and uses GetUrl() to create links. The result is a menu that can be changed through data rather than by rewriting the page’s HTML.
Glossary
| Term | Non-programmer explanation |
|---|---|
| Active menu item | A menu record whose IsActive value is true, meaning it is currently eligible to appear in the menu. |
| Application | The running website and the code that receives requests, loads information, and produces responses. |
| Cache | Temporary stored information used so the application does not have to prepare or retrieve the same information repeatedly. |
| Child menu item | A menu record displayed underneath another menu item. Its ParentMenuItemID points to the parent record. |
| Content | Information shown to a visitor, such as an article, diagnosis explanation, image, or other material associated with a menu item. |
| Database | An organized storage system containing the records used by the application, including menu, visitor, route, and content information. |
| Dynamic menu | A menu whose labels, hierarchy, visibility, order, and links are supplied by data rather than being written as fixed HTML. |
| Entity Framework Core | A .NET library that lets C# code read database records through objects and queries. |
| Hierarchy | The parent-and-child arrangement that determines which menu items appear beneath other items. |
IMemoryCache |
The .NET service used by the partial to temporarily retain a menu tree in the web application’s memory. |
| Layout | A shared page template that provides common elements such as the header, visitor tabs, menu, and page body. |
MenuItem |
One database record representing a possible navigation entry. It contains the title, parent, URL, visibility mask, order, and active status. |
MenuItemID |
The unique number identifying one menu record. |
MenuTree |
The in-memory collection of active MenuItem records passed from the page model to the layout and menu partial. |
MenuViewModel |
A small C# object that carries menu and visitor information into _Menu.cshtml. |
| Parent menu item | A menu record that can contain child records beneath it. A top-level parent has no ParentMenuItemID. |
| Partial view | A reusable Razor template, such as _Menu.cshtml, that can be inserted into a layout or page. |
| Persona | The visitor’s selected audience or role, such as Patient, Caregiver, Provider, Pharmaceutical, or Visitor. |
PageBase |
The shared page-model class that loads common lookup data, resolves the current visitor, and loads active menu records. |
ParentMenuItemID |
The database value connecting a child menu item to its parent. A blank value means the item is at the top level. |
| Recursive rendering | A technique where the menu renderer calls the same child-rendering logic again for each child, allowing multiple levels of nested menus. |
RouteType |
A database classification describing the general kind of destination, such as Visitor, Content, Page, or Redirect. |
SortOrder |
The number used to arrange menu items among other items with the same parent. |
| Session | Short-lived server-side information associated with one visitor’s browser. This proof of concept uses it to remember the selected visitor code. |
RouteTarget |
Route string stored on a menu item and interpreted by the application. |
| Visitor code | The short code representing a visitor type, such as P for Patient or C for Caregiver. |
| Visitor mask | A compact string listing visitor codes allowed to see a menu item, such as PC or PCMDV. |
| Visitor type | A database lookup record describing an audience category and its display order. |
_Layout.cshtml |
The shared Razor layout that prepares the menu view model and invokes _Menu.cshtml. |
_Menu.cshtml |
The shared Razor partial that filters, orders, nests, caches, and renders the menu records as HTML. |
GetUrl() |
The POC helper that returns the application URL for a menu item; the database column is RouteTarget. |