Click here to Skip to main content
15,878,814 members
Articles / Web Development / HTML
Tip/Trick

So You Want to Have CMS Functionality In Your MVC Project?

Rate me:
Please Sign up or sign in to vote.
4.77/5 (19 votes)
27 Oct 2014CPOL2 min read 40.1K   1K   35   13
A project demonstrating the technologies 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 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:

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

Index View:

C#
@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:

C#
Install-Package MvcSiteMapProvider.MVC5 

Next update Mvc.sitemap. The following will add a home page with dynamic nodes:

XML
<?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.

Let's create BaseSiteMapNode:

C#
/// <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"}
                    }
        },
        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:

C#
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:

C#
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:

C#
routes.MapRoute(
                name: "CmsRoute",
                url: "{*permalink}",
                defaults: new { controller = "Page", action = "Index" },
                constraints: new { permalink = new BaseRoutingConstraint() }
            );

BaseRoutingConstraint

C#
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:

C#
ViewEngines.Engines.Clear();
ViewEngines.Engines.Insert(0, new CMSViewEngine());

Add the following ViewEngine:

C#
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 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:

C#
@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:

C#
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;
                        case "id4":
                            // Load Widget
                            // This is hard coded, we could add more parameters to RenderZone
                            // and load controllers & Actions from db for this zone & Page Id
                            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:

XML
<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

C#
public class PostsController : Controller
    {
        // GET: Posts
        public ActionResult DisplayPosts(int id)
        {
            // This should probably be retrieved from the database
            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

C#
@using RoutingTest.Models

@model Post

@{
    Layout = "";
}

<h1>@Model.Title</h1>
<b>@Model.Message</b>

History

  • Article created
  • Added widgets
  • Spelling

License

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


Written By
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

 
QuestionHave you consider to post this as a tip? Pin
Nelek15-Sep-15 0:08
protectorNelek15-Sep-15 0:08 
AnswerRe: Have you consider to post this as a tip? Pin
Laredo Tirnanic15-Sep-15 1:20
Laredo Tirnanic15-Sep-15 1:20 
GeneralRe: Have you consider to post this as a tip? Pin
Nelek15-Sep-15 3:16
protectorNelek15-Sep-15 3:16 
Questionrazor code in Render Each Page's Content Pin
samrobles26-Jul-15 6:15
samrobles26-Jul-15 6:15 
AnswerRe: razor code in Render Each Page's Content Pin
Laredo Tirnanic26-Jul-15 21:53
Laredo Tirnanic26-Jul-15 21:53 
GeneralRe: razor code in Render Each Page's Content Pin
samrobles27-Jul-15 16:10
samrobles27-Jul-15 16:10 
QuestionAwesome Pin
damodara naidu betha20-Nov-14 2:28
damodara naidu betha20-Nov-14 2:28 
Really interesting. Thank you very much for sharing your knowledge. Pls keep posting.
GeneralMy vote of 5 Pin
majid torfi27-Oct-14 19:30
professionalmajid torfi27-Oct-14 19:30 
QuestionNice approach Pin
RobDeVoer19-Aug-14 11:27
RobDeVoer19-Aug-14 11:27 
AnswerRe: Nice approach Pin
Laredo Tirnanic20-Aug-14 3:54
Laredo Tirnanic20-Aug-14 3:54 
QuestionDownload not available Pin
DarrollWalsh19-Aug-14 3:31
DarrollWalsh19-Aug-14 3:31 
AnswerRe: Download not available Pin
Laredo Tirnanic19-Aug-14 4:15
Laredo Tirnanic19-Aug-14 4:15 
GeneralRe: Download not available Pin
DarrollWalsh19-Aug-14 7:48
DarrollWalsh19-Aug-14 7:48 

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

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