Skip to content

Commit f4051f5

Browse files
committed
Introduced reusable LayoutController
This provides a base controller for returning `PartialViewResult`s related to different aspects of a `_layout.cshtml`. Out of the box, the `LayoutController` includes a `Menu()` action for writing the top-tier navigation. It also includes several helper methods that make it trivial to add additional actions as needed (e.g., `Footer()`, AncillaryNavigation()`, &c.).
1 parent 89ab030 commit f4051f5

3 files changed

Lines changed: 238 additions & 0 deletions

File tree

Ignia.Topics.Tests/TopicControllerTest.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
using Ignia.Topics.Web.Mvc.Controllers;
1515
using Ignia.Topics.ViewModels;
1616
using System.Web.Mvc;
17+
using System.Web.Routing;
18+
using Ignia.Topics.Mapping;
19+
using Ignia.Topics.Web.Mvc.Models;
1720

1821
namespace Ignia.Topics.Tests {
1922

@@ -164,6 +167,35 @@ public void SitemapController_Index() {
164167

165168
}
166169

170+
/*==========================================================================================================================
171+
| TEST: MENU
172+
\-------------------------------------------------------------------------------------------------------------------------*/
173+
/// <summary>
174+
/// Triggers the <see cref="FallbackController.Index()" /> action.
175+
/// </summary>
176+
[TestMethod]
177+
public void LayoutController_Menu() {
178+
179+
var routes = new RouteData();
180+
var uri = new Uri("http://localhost/Web/Web_0/Web_0_1/Web_0_1_1");
181+
var topic = _topicRepository.Load("Root:Web:Web_0:Web_0_1:Web_0_1_1");
182+
183+
var topicRoutingService = new MvcTopicRoutingService(_topicRepository, uri, routes);
184+
var mappingService = new TopicMappingService(_topicRepository);
185+
186+
var controller = new LayoutController<NavigationTopicViewModel>(_topicRepository, topicRoutingService, mappingService);
187+
var result = controller.Menu() as PartialViewResult;
188+
var model = result.Model as NavigationViewModel<NavigationTopicViewModel>;
189+
190+
Assert.IsNotNull(model);
191+
Assert.AreEqual<string>(topic.GetUniqueKey(), model.CurrentKey);
192+
Assert.AreEqual<string>("Root:Web", model.NavigationRoot.UniqueKey);
193+
Assert.AreEqual<int>(3, model.NavigationRoot.Children.Count());
194+
Assert.IsTrue(model.NavigationRoot.IsSelected(topic.GetUniqueKey()));
195+
196+
}
197+
198+
167199
} //Class
168200

169201
} //Namespace
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/*==============================================================================================================================
2+
| Author Ignia, LLC
3+
| Client Ignia, LLC
4+
| Project Topics Library
5+
\=============================================================================================================================*/
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
using System.Web.Mvc;
10+
using Ignia.Topics;
11+
using Ignia.Topics.Mapping;
12+
using Ignia.Topics.Repositories;
13+
using Ignia.Topics.Web.Mvc.Models;
14+
15+
namespace Ignia.Topics.Web.Mvc.Controllers {
16+
17+
/*============================================================================================================================
18+
| CLASS: LAYOUT CONTROLLER
19+
\---------------------------------------------------------------------------------------------------------------------------*/
20+
/// <summary>
21+
/// Provides access to views for populating specific layout dependencies, such as the <see cref="Menu"/>.
22+
/// </summary>
23+
/// <remarks>
24+
/// As a best practice, global data required by the layout view are requested independently of the current page. This allows
25+
/// each layout element to be provided with its own layout data, in the form of <see cref="NavigationViewModel{T}"/>s,
26+
/// instead of needing to add this data to every view model returned by <see cref="TopicController"/>. The <see
27+
/// cref="LayoutController{T}"/> facilitates this by not only providing a default implementation for <see cref="Menu"/>, but
28+
/// additionally providing protected helper methods that aid in locating and assembling <see cref="Topic"/> and <see
29+
/// cref="INavigationTopicViewModelCore"/> references that are relevant to specific layout elements.
30+
/// </remarks>
31+
public class LayoutController<T> : Controller where T : class, INavigationTopicViewModelCore, new() {
32+
33+
/*==========================================================================================================================
34+
| PRIVATE VARIABLES
35+
\-------------------------------------------------------------------------------------------------------------------------*/
36+
private readonly ITopicRepository _topicRepository = null;
37+
private readonly ITopicRoutingService _topicRoutingService = null;
38+
private readonly ITopicMappingService _topicMappingService = null;
39+
private Topic _currentTopic = null;
40+
41+
/*==========================================================================================================================
42+
| CONSTRUCTOR
43+
\-------------------------------------------------------------------------------------------------------------------------*/
44+
/// <summary>
45+
/// Initializes a new instance of a Topic Controller with necessary dependencies.
46+
/// </summary>
47+
/// <returns>A topic controller for loading OnTopic views.</returns>
48+
public LayoutController(
49+
ITopicRepository topicRepository,
50+
ITopicRoutingService topicRoutingService,
51+
ITopicMappingService topicMappingService
52+
) : base() {
53+
_topicRepository = topicRepository;
54+
_topicRoutingService = topicRoutingService;
55+
_topicMappingService = topicMappingService;
56+
}
57+
58+
/*==========================================================================================================================
59+
| TOPIC REPOSITORY
60+
\-------------------------------------------------------------------------------------------------------------------------*/
61+
/// <summary>
62+
/// Provides a reference to the Topic Repository in order to gain arbitrary access to the entire topic graph.
63+
/// </summary>
64+
/// <returns>The TopicRepository associated with the controller.</returns>
65+
protected ITopicRepository TopicRepository {
66+
get {
67+
return _topicRepository;
68+
}
69+
}
70+
71+
/*==========================================================================================================================
72+
| CURRENT TOPIC
73+
\-------------------------------------------------------------------------------------------------------------------------*/
74+
/// <summary>
75+
/// Provides a reference to the current topic associated with the request.
76+
/// </summary>
77+
/// <returns>The Topic associated with the current request.</returns>
78+
protected Topic CurrentTopic {
79+
get {
80+
if (_currentTopic == null) {
81+
_currentTopic = _topicRoutingService.GetCurrentTopic();
82+
}
83+
return _currentTopic;
84+
}
85+
}
86+
87+
/*==========================================================================================================================
88+
| MENU
89+
\-------------------------------------------------------------------------------------------------------------------------*/
90+
/// <summary>
91+
/// Provides the global menu for the site layout, which exposes the top two tiers of navigation.
92+
/// </summary>
93+
public virtual PartialViewResult Menu() {
94+
95+
/*------------------------------------------------------------------------------------------------------------------------
96+
| Establish variables
97+
\-----------------------------------------------------------------------------------------------------------------------*/
98+
var currentTopic = CurrentTopic;
99+
var navigationRootTopic = (Topic)null;
100+
101+
/*------------------------------------------------------------------------------------------------------------------------
102+
| Identify navigation root
103+
>-------------------------------------------------------------------------------------------------------------------------
104+
| The navigation root in the case of the main menu is the namespace; i.e., the first topic underneath the root.
105+
\-----------------------------------------------------------------------------------------------------------------------*/
106+
navigationRootTopic = GetNavigationRoot(currentTopic, 2, "Web");
107+
108+
/*------------------------------------------------------------------------------------------------------------------------
109+
| Construct view model
110+
\-----------------------------------------------------------------------------------------------------------------------*/
111+
var navigationViewModel = new NavigationViewModel<T>() {
112+
NavigationRoot = AddNestedTopics(navigationRootTopic, false, 3),
113+
CurrentKey = CurrentTopic?.GetUniqueKey()
114+
};
115+
116+
/*------------------------------------------------------------------------------------------------------------------------
117+
| Return the corresponding view
118+
\-----------------------------------------------------------------------------------------------------------------------*/
119+
return PartialView(navigationViewModel);
120+
121+
}
122+
123+
/*==========================================================================================================================
124+
| GET NAVIGATION ROOT
125+
\-------------------------------------------------------------------------------------------------------------------------*/
126+
/// <summary>
127+
/// A helper function that will crawl up the current tree and retrieve the topic that is <paramref name="fromRoot"/> tiers
128+
/// down from the root of the topic graph.
129+
/// </summary>
130+
/// <remarks>
131+
/// Often, an action of a <see cref="LayoutController{T}"/> will need a reference to a topic at a certain level, which
132+
/// represents the navigation for the site. For instance, if the primary navigation is at <c>Root:Web</c>, then the
133+
/// navigation is one level from the root (i.e., <paramref name="fromRoot"/>=1). This, however, should not be hard-coded
134+
/// in case a site has multiple roots. For instance, if a page is under <c>Root:Library</c> then <i>that</i> should be the
135+
/// navigation root. This method provides support for these scenarios.
136+
/// </remarks>
137+
/// <param name="currentTopic">The <see cref="Topic"/> to start from.</param>
138+
/// <param name="fromRoot">The distance that the navigation root should be from the root of the topic graph.</param>
139+
/// <param name="defaultRoot">If a root cannot be identified, the default root that should be returned.</param>
140+
protected Topic GetNavigationRoot(Topic currentTopic, int fromRoot = 2, string defaultRoot = "Web") {
141+
var navigationRootTopic = currentTopic;
142+
while (DistanceFromRoot(navigationRootTopic) > fromRoot) {
143+
navigationRootTopic = navigationRootTopic.Parent;
144+
}
145+
146+
if (navigationRootTopic == null && !String.IsNullOrWhiteSpace(defaultRoot)) {
147+
navigationRootTopic = TopicRepository.Load(defaultRoot);
148+
}
149+
150+
return navigationRootTopic;
151+
152+
}
153+
154+
/*==========================================================================================================================
155+
| DISTANCE FROM ROOT
156+
\-------------------------------------------------------------------------------------------------------------------------*/
157+
/// <summary>
158+
/// A helper function that will determine how far a given topic is from the root of a tree.
159+
/// </summary>
160+
/// <param name="sourceTopic">The <see cref="Topic"/> to pull the values from.</param>
161+
private int DistanceFromRoot(Topic sourceTopic) {
162+
var distance = 1;
163+
while (sourceTopic.Parent != null) {
164+
sourceTopic = sourceTopic.Parent;
165+
distance++;
166+
}
167+
return distance;
168+
}
169+
170+
/*==========================================================================================================================
171+
| ADD NESTED TOPICS
172+
\-------------------------------------------------------------------------------------------------------------------------*/
173+
/// <summary>
174+
/// A helper function that allows a set number of tiers to be added to a <see cref="NavigationViewModel"/> tree.
175+
/// </summary>
176+
/// <param name="sourceTopic">The <see cref="Topic"/> to pull the values from.</param>
177+
/// <param name="allowPageGroups">Determines whether <see cref="PageGroupTopicViewModel"/>s should be crawled.</param>
178+
/// <param name="tiers">Determines how many tiers of children should be included in the graph.</param>
179+
protected T AddNestedTopics(
180+
Topic sourceTopic,
181+
bool allowPageGroups = true,
182+
int tiers = 1
183+
) {
184+
tiers--;
185+
if (sourceTopic == null) {
186+
return null as T;
187+
}
188+
var viewModel = _topicMappingService.Map<T>(sourceTopic, Relationships.None);
189+
if (tiers >= 0 && (allowPageGroups || !sourceTopic.ContentType.Equals("PageGroup"))) {
190+
foreach (var topic in sourceTopic.Children.Sorted.Where(t => t.IsVisible())) {
191+
viewModel.Children.Add(
192+
AddNestedTopics(
193+
topic,
194+
allowPageGroups,
195+
tiers
196+
)
197+
);
198+
}
199+
}
200+
return viewModel;
201+
}
202+
203+
} // Class
204+
205+
} // Namespace

Ignia.Topics.Web.Mvc/Ignia.Topics.Web.Mvc.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
<ItemGroup>
6666
<Compile Include="Controllers\ErrorController.cs" />
6767
<Compile Include="Controllers\FallbackController.cs" />
68+
<Compile Include="Controllers\LayoutController.cs" />
6869
<Compile Include="Controllers\RedirectController.cs" />
6970
<Compile Include="Controllers\SitemapController.cs" />
7071
<Compile Include="Controllers\TopicController.cs" />

0 commit comments

Comments
 (0)