Introduction
I used the following project to research a few technologies that I could use to add very basic CMS capabilities to my projects.
Please note this is not a complete CMS, it's merely an introduction to the technologies which can be used to add CMS functionalities to your projects.
The aim of this project was to add the following functionality:
- Add a common controller that would handle paging
- Dynamic SiteMap Provider (Nodes are read from a datasource)
- Dynamic Routing (i.e., Routes such as "~/Misc" and "~/Misc/Help" would point to a single page controller)
- Apply page templates
- Render each page's content
- Display Widgets
Using the Code
Common Paging Controller
This controller will display our pages. Paging controller:
public class PageController : Controller
{
public ActionResult Index()
{
this.ViewBag.Title = SiteMaps.Current.CurrentNode.Title;
return View();
}
}
Index View:
@using MvcSiteMapProvider;
@using RoutingTest.Controllers;
@using RoutingTest.Models.ViewPage;
@{
string a = "Hello";
}
<h2>@ViewBag.Title</h2>
@Html.MvcSiteMap().SiteMapPath()
<br />
@RenderZone("Content");
Dynamic Sitemap Provider
I decided to use MvcSiteMapProvider
(MVC5). Install the package:
Install-Package MvcSiteMapProvider.MVC5
Next update Mvc.sitemap
. The following will add a home page with dynamic nodes:
="1.0"="utf-8"
<mvcSiteMap xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-4.0"
xsi:schemaLocation="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-4.0 MvcSiteMapSchema.xsd">
<mvcSiteMapNode title="Home" controller="Home"
action="Index" key="00000000-0000-0000-0000-000000000001"
Template="~/Views/Shared/_Layout.cshtml">
<mvcSiteMapNode title="Dynamic Nodes"
dynamicNodeProvider="nFrall.Core.Web.Base.SiteMap.BaseSiteMapNode, RoutingTest" />
</mvcSiteMapNode>
</mvcSiteMap>
BaseSiteMapNode
is referenced in the node definition. This will generate nodes dynamically from a datasource
.
Let's create BaseSiteMapNode
:
public class BaseSiteMapNode : DynamicNodeProviderBase
{
public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
{
return ReturnAll();
}
public static IEnumerable<DynamicNode> ReturnAll()
{
var nodes = new[] {
new DynamicNode
{
Title = "Misc",
ParentKey = "00000000-0000-0000-0000-000000000001",
Key = "id1",
Controller = "Page",
Action = "Index",
Url = "~/Misc",
Attributes = new Dictionary<string, object>
{
{"Template", "~/Views/Shared/_LayoutBlue.cshtml"},
}
}
,
new DynamicNode
{
Title = "Help",
ParentKey = "00000000-0000-0000-0000-000000000001",
Key = "id2",
Controller = "Page",
Action = "Index",
Url = "~/Misc/Help",
Attributes = new Dictionary<string, object>
{
{"Template", "~/Views/Shared/_Layout.cshtml"}
}
},
new DynamicNode
{
Title = "Base Page",
ParentKey = "00000000-0000-0000-0000-000000000001",
Key = "id3",
Controller = "Page",
Action = "Index",
Url = "~/Page/Index",
Attributes = new Dictionary<string, object>
{
{"Template", "~/Views/Shared/_LayoutBlue.cshtml"}
}
},
new DynamicNode
{
Title = "Widgets: Display Posts",
ParentKey = "00000000-0000-0000-0000-000000000001",
Key = "id4",
Controller = "Page",
Action = "Index",
Url = "~/DisplayPost/Index",
Attributes = new Dictionary<string, object>
{
{"Template", "~/Views/Shared/_LayoutBlue.cshtml"}
},
}
};
nodes[0].RouteValues.Add("id", 1);
nodes[1].RouteValues.Add("id", 2);
nodes[2].RouteValues.Add("id", 3);
return nodes;
}
}
ReturnAll()
should generate the sitemap nodes from a database.
Include the following in your view to generate a menu
and a breadcrumb
:
Menu
@Html.MvcSiteMap().Menu(false, true, true)
BreadCrumb
@Html.MvcSiteMap().SiteMapPath()
Please see https://github.com/maartenba/MvcSiteMapProvider for more information.
Dynamic Routing
In the previous step, we implemented the sitemap provider. The menu
& breadcrumb
trail would be rendered but the specified URLs would be ignored. i.e., typing http://localhost/Misc or http://localhost/Misc/Help would result in 404 errors.
To fix this, we need to update the project's routing. One option for this would be to update RouteConfig.cs with the following:
routes.MapRoute(
"Misc",
"Misc",
new { controller = "Page", action = "Index" }
);
routes.MapRoute(
"Help",
"Misc/Help/{username}",
new { controller = "Page", action = "Index", username= UrlParameter.Optional }
);
As a result of this http://localhost/Misc & http://localhost/Misc/Help would point to the controller "Page
" with action "Index
". This is a cumbersome approach as each item's route needs to be added. A better way is to use IRouteConstraint
.
Change RouteConfig.cs to the following:
routes.MapRoute(
name: "CmsRoute",
url: "{*permalink}",
defaults: new { controller = "Page", action = "Index" },
constraints: new { permalink = new BaseRoutingConstraint() }
);
BaseRoutingConstraint
public class BaseRoutingConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route,
string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
if (values[parameterName] != null)
{
var permalink = string.Format( "~/{0}",values[parameterName].ToString());
return BaseSiteMapNode.ReturnAll().Any(a => a.Url == permalink);
}
return false;
}
}
If Match()
returns true
, the route cmsRoute
is used. If not, the next route is tried. Note that I searched BaseSiteMapNode.ReturnAll()
and not the sitemap nodes. Unfortunately, this would have resulted in an error. SitemapNodes
should not be used to determine routing.
Apply Page Templates
Add the following to the Global.asax.cs's Application_Start
:
ViewEngines.Engines.Clear();
ViewEngines.Engines.Insert(0, new CMSViewEngine());
Add the following ViewEngine
:
public class CMSViewEngine : RazorViewEngine
{
public CMSViewEngine()
: base()
{
}
protected override IView CreateView(ControllerContext controllerContext,
string viewPath, string masterPath)
{
if (MvcSiteMapProvider.SiteMaps.Current.CurrentNode != null)
{
masterPath = MvcSiteMapProvider.SiteMaps.Current.CurrentNode.Attributes
["Template"].ToString();
}
return base.CreateView(controllerContext, viewPath, masterPath);
}
protected override IView CreatePartialView
(ControllerContext controllerContext, string partialPath)
{
return base.CreatePartialView(controllerContext, partialPath);
}
protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
{
return base.FileExists(controllerContext, virtualPath);
}
}
Note that the CreateView
method uses the current sitemap node's template attribute to assign a new layout.
Render Each Page's Content
Let's add a zone called "Content
" to the page's index view as follows:
@using MvcSiteMapProvider;
@using RoutingTest.Controllers;
@using RoutingTest.Models.ViewPage;
@{
string a = "Hello";
}
<h2>@ViewBag.Title</h2>
@Html.MvcSiteMap().SiteMapPath()
<br />
@RenderZone("Content");
I've created a new CMSViewPage
which exposes the RenderZone
method. Here's the code:
public abstract class CMSViewPage<TModel> : System.Web.Mvc.WebViewPage<TModel>
{
public static MvcHtmlString RenderZone(string zone)
{
string result = "<h1>None</h1>";
switch (zone)
{
case "Content":
switch (SiteMaps.Current.CurrentNode.Key)
{
case "id1":
result = "<b>Misc Content</b>: blah.........................";
break;
case "id2":
result = "<b>Help me Content</b>: blah.........................";
break;
case "id3":
result = "<b>Generic Page Content</b>: blah.........................";
break;
case "id4":
result = this.Html.Action
("DisplayPosts", "Posts", new { id = 1 }).ToHtmlString();
break;
default:
result = "<b>Content</b>Nothing.......................";
break;
};
break;
default:
break;
}
return new MvcHtmlString(result);
}
}
To replace the default viewpage, change the <pages>
tag to the following in Views/Web.config:
<pages pageBaseType="RoutingTest.Models.ViewPage.CMSViewPage">
Display Widgets
Widgets are merely just controllers with actions. In CMSViewPage
, we are calling PostsController
with a DisplayPosts
action for "id4
".
PostsController
public class PostsController : Controller
{
public ActionResult DisplayPosts(int id)
{
Post post = new Post();
if (id == 1)
{
post.Id = 1;
post.Title = "Post 1";
post.Message = "My First Post!!!!";
}
else if (id == 2)
{
post.Id = 2;
post.Title = "Post 2";
post.Message = "My Second Post!!!!";
}
return this.PartialView(post);
}
}
DisplayPosts View
@using RoutingTest.Models
@model Post
@{
Layout = "";
}
<h1>@Model.Title</h1>
<b>@Model.Message</b>
History
- Article created
- Added widgets
- Spelling