Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

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

0.00/5 (No votes)
27 Oct 2014 1  
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:

public class PageController : Controller
{
   //
   // GET: /Page/
   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:

<?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:

/// <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:

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()
        {
        }

        /// <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:

@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;
                        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:

<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
    {
        // 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

@using RoutingTest.Models

@model Post

@{
    Layout = "";
}

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

History

  • Article created
  • Added widgets
  • Spelling

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here