Click here to Skip to main content
Click here to Skip to main content
Go to top

So you want to have CMS functionality in your MVC Project?

, 20 Aug 2014
Rate this:
Please Sign up or sign in to vote.
A project demonstrating the technology's used to write a simple MVC CMS

Introduction

I used the following project to research a few technologies that I could use to add very basic CMS capablities 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

Using the code

Common Paging Controller

This controller will display our pages. Paging controller:

public class PageController : Controller
{
   //
   // GET: /Page/
   public ActionResult Index()
   {
       this.ViewBag.Title = SiteMaps.Current.CurrentNode.Title;
       return View();
   }
}

 Index View:

@using MvcSiteMapProvider;

@{

}

<h2>@ViewBag.Title</h2>

Dynamic Sitemap Provider

I decide 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:

<?xml version="1.0" encoding="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.

Lets create BaseSiteMapNode:

    /// <summary>
    /// A base DynamicSiteMapNode
    /// </summary>
    public class BaseSiteMapNode : DynamicNodeProviderBase
    {
        /// <summary>
        /// Gets the dynamic node collection.
        /// </summary>
        /// <param name="node">The node.</param>
        /// <returns></returns>
        public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
        {
            return ReturnAll();
        }

        /// <summary>
        /// Returns all.
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<DynamicNode> ReturnAll()
        {
            //Todo: Load From Database
            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"}
                        }
            }
         };
            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's & breadcrumb trail would be rendered but the specified url's 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 Globa.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()
        {
        }

        /// <summary>
        /// Creates a view by using the specified controller context and the paths of the view and master view.
        /// If the page has a sitemap node, it's Layout will be used
        /// </summary>
        /// <param name="controllerContext">The controller context.</param>
        /// <param name="viewPath">The path to the view.</param>
        /// <param name="masterPath">The path to the master view.</param>
        /// <returns>
        /// The view.
        /// </returns>
        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 the CreateView method uses the current sitemap node's template attribute to assign a new layout.

Render Each Page's Content

Lets add a zone called "Content" to the page's index view as follows:

@using MvcSiteMapProvider;
@using RoutingTest.Controllers;
@using RoutingTest.Models.ViewPage;

@{
    //ViewBag.Title = "Index";
    //Layout = "~/Views/Shared/_LayoutBlue.cshtml";
    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)
        {
            //Todo: Load this data from the database
            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;
                        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">

I've just implemented Zones, but Zones could contain widgets, etc. It should be trivial to extend this functionality.

History

Article Created

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Laredo Tirnanic
Software Developer (Senior) Nobelis Software Solutions
South Africa South Africa
I've been developing in .Net since 2002. I specialize in web application development.

Comments and Discussions

 
QuestionNice approach PinmemberRobDeVoer19-Aug-14 11:27 
AnswerRe: Nice approach PinmemberLaredo Tirnanic20-Aug-14 3:54 
QuestionDownload not available PinmemberDarrol19-Aug-14 3:31 
AnswerRe: Download not available [modified] PinmemberLaredo Tirnanic19-Aug-14 4:15 
GeneralRe: Download not available PinmemberDarrollWalsh19-Aug-14 7:48 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140922.1 | Last Updated 20 Aug 2014
Article Copyright 2014 by Laredo Tirnanic
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid