The Ignia.Topics.Web.Mvc
assembly provides an implementation of OnTopic for use with the ASP.NET MVC 5.x Framework.
There are three key components at the heart of the MVC implementation.
MvcTopicRoutingService
: This is a concrete implementation of theITopicRoutingService
which accepts contextual information about a given request (in this case, the URL and routing data) and then uses it to retrieve the currentTopic
from anITopicRepository
.TopicController
: This is a default controller instance that can be used for any topic path. It will automatically validate that theTopic
exists, that it is not disabled (IsDisabled
), and will honor any redirects (e.g., if theUrl
attribute is filled out). Otherwise, it will returnTopicViewResult
based on a view model, view name, and content type.TopicViewEngine
: TheTopicViewEngine
is called every time a view is requested. It works in conjunction withTopicViewResult
to identify matching MVC views based on predetermined locations and conventions. These are discussed below.
There are six main controllers that ship with the MVC implementation. In addition to the core TopicController
, these include the following ancillary controllers:
ErrorControllerBase<T>
: Provides support forError
,NotFound
, andInternalServer
actions. Can accept anyIPageTopicViewModel
as a generic argument; that will be used as the view model.FallbackController
: Used in a Controller Factory as a fallback, in case no other controllers can accept the request. Simply returns aNotFoundResult
with a predefined message.LayoutControllerBase<T>
: Provides support for a navigation menu by automatically mapping the top three tiers of the current namespace (e.g.,Web
, its children, and grandchildren). Can accept anyINavigationTopicViewModel
as a generic argument; that will be used as the view model for each mapped instance.RedirectController
: Provides a singleRedirect
action which can be bound to a route such as/Topic/{ID}/
; this provides support for permanent URLs that are independent of theGetWebPath()
.SitemapController
: Provides a singleSitemap
action which returns a reference to theITopicRepository
, thus allowing a sitemap view to recurse over the entire Topic graph, including all attributes.
Note: There is not a practical way for MVC to provide routing for generic controllers. As such, these must be subclassed by each implementation. The derived controller needn't do anything outside of provide a specific type reference to the generic base.
By default, OnTopic matches views based on the current topic's ContentType
and, if available, View
.
There are multiple ways for a view to be set. The TopicViewResult
will automatically evaluate views based on the following locations. The first one to match a valid view name is selected.
?View=
query string parameter (e.g.,?View=Accordion
)Accept
headers (e.g.,Accept=application/json
); will treat the segment after the/
as a possible view nameView
attribute (i.e.,topic.View
)ContentType
attribute (i.e.,topic.ContentType
)
For each of the above View Matching rules, the TopicViewEngine
will search the following locations for a matching view:
~/Views/{ContentType}/{View}.cshtml
~/Views/ContentTypes/{ContentType}.{View}.cshtml
~/Views/ContentTypes/{ContentType}.cshtml
~/Views/Shared/{View}.cshtml
Note: After searching each of these locations for each of the View Matching rules, control will be handed over to the
RazorViewEngine
, which will search the out-of-the-box default locations for ASP.NET MVC.
If the topic.ContentType
is ContentList
and the Accept
header is application/json
then the TopicViewResult
and TopicViewEngine
would coordinate to search the following paths:
~/Views/ContentList/JSON.cshtml
~/Views/ContentTypes/ContentList.JSON.cshtml
~/Views/ContentTypes/JSON.cshtml
~/Views/Shared/JSON.cshtml
If no match is found, then the next Accept
header will be searched. Eventually, if no match can be found on the various View Matching rules, then the following will be searched:
~/Views/ContentList/ContentList.cshtml
~/Views/ContentTypes/ContentList.ContentList.cshtml
~/Views/ContentTypes/ContentList.cshtml
~/Views/Shared/ContentList.cshtml
In the global.asax.cs
, the following components should be registered under the Application_Start
event handler:
ControllerBuilder.Current.SetControllerFactory(new OrganizationNameControllerFactory());
ViewEngines.Engines.Insert(0, new TopicViewEngine());
Note: The controller factory name is arbitrary, and should follow the conventions appropriate for the site. Ignia typically uses
{OrganizationName}ControllerFactory
(e.g.,IgniaControllerFactory
), but OnTopic doesn't need to know or care what the name is; that is between your application and the ASP.NET MVC Framework.
When registering routes via RouteConfig.RegisterRoutes()
(typically via the RouteConfig
class), register a route for any OnTopic routes:
routes.MapRoute(
name: "WebTopics",
url: "Web/{*path}",
defaults: new { controller = "Topic", action = "Index", id = UrlParameter.Optional, rootTopic = "Web" }
);
Note: Because OnTopic relies on wildcard pathnames, a new route should be configured for every root namespace (e.g.,
/Web
). While it's possible to configure OnTopic to evaluate all paths, this makes it difficult to delegate control to other controllers and handlers, when necessary.
As OnTopic relies on constructor injection, the application must be configured in a Composition Root—in the case of ASP.NET MVC, that means a custom controller factory. The basic structure of this might look like:
var connectionString = ConfigurationManager.ConnectionStrings["OnTopic"].ConnectionString;
var sqlTopicRepository = new SqlTopicRepository(connectionString);
var cachedTopicRepository = new CachedTopicRepository(sqlTopicRepository);
var topicViewModelLookupService = new TopicViewModelLookupService();
var topicMappingService = new TopicMappingService(cachedTopicRepository, topicViewModelLookupService);
var mvcTopicRoutingService = new MvcTopicRoutingService(
cachedTopicRepository,
requestContext.HttpContext.Request.Url,
requestContext.RouteData
);
switch (controllerType.Name) {
case nameof(TopicController):
return new TopicController(sqlTopicRepository, mvcTopicRoutingService, topicMappingService);
case default:
return base.GetControllerInstance(requestContext, controllerType);
}
For a complete reference template, including the ancillary controllers, see the OrganizationNameControllerFactory.cs
Gist.
Note: The default
TopicController
will automatically identify the current topic (based on e.g. the URL), map the current topic to a corresponding view model (based on theTopicMappingService
conventions), and then return a corresponding view (based on the view conventions). For most applications, this is enough. If custom mapping rules or additional presentation logic are needed, however, implementors can subclassTopicController
.