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 theITopicRoutingServicewhich accepts contextual information about a given request (in this case, the URL and routing data) and then uses it to retrieve the currentTopicfrom anITopicRepository.TopicController: This is a default controller instance that can be used for any topic path. It will automatically validate that theTopicexists, that it is not disabled (IsDisabled), and will honor any redirects (e.g., if theUrlattribute is filled out). Otherwise, it will returnTopicViewResultbased on a view model, view name, and content type.TopicViewEngine: TheTopicViewEngineis called every time a view is requested. It works in conjunction withTopicViewResultto 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, andInternalServeractions. Can accept anyIPageTopicViewModelas 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 aNotFoundResultwith 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 anyINavigationTopicViewModelas a generic argument; that will be used as the view model for each mapped instance.RedirectController: Provides a singleRedirectaction 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 singleSitemapaction 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)Acceptheaders (e.g.,Accept=application/json); will treat the segment after the/as a possible view nameViewattribute (i.e.,topic.View)ContentTypeattribute (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
TopicControllerwill automatically identify the current topic (based on e.g. the URL), map the current topic to a corresponding view model (based on theTopicMappingServiceconventions), 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.