Simple CMS with Linq to SQL part II-a

This isn’t a new post per-se, but a few refinements to our existing project, including added comments in the new classes, before proceeding further. I pretty much rushed through to getting content adding, editing deleting functionality so let’s see how much we can smooth the edges before moving on…

First, I added a simple code file called Summaries.cs to the Models folder with the following…

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using SimpleCMS.Models;

namespace SimpleCMS.Models
{
	public class PageSummary : ContentPage { }

	public class CommentSummary : ContentComment { }
}

All this does is allow us to create an anonymous type wrapper for use in page and content summaries without getting all the associated fields in the object. It’s a performance thing.

Next, the modified PageView class in the Models folder.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using SimpleCMS.Helpers;

namespace SimpleCMS.Models
{
    public class PageView
    {
		/// <summary>
		/// Current viewing page
		/// </summary>
		public ContentPage Page { get; set; }

		/// <summary>
		/// Paged index list of pages
		/// </summary>
		public PagedList<ContentPage> Pages { get; set; }

		/// <summary>
		/// Paged index list of pages
		/// </summary>
		public PagedList<PageSummary> PageSummary { get; set; }


		/// <summary>
		/// Paged list of comments in the current page
		/// </summary>
		public PagedList<ContentComment> Comments { get; set; }

		/// <summary>
		/// Paged list of comments in the current page
		/// </summary>
		public PagedList<CommentSummary> CommentSummary { get; set; }

		/// <summary>
		/// This is for breadcrumb navigation use
		/// </summary>
		public Dictionary<int, string> Parents { get; set; }

		/// <summary>
		/// Constructor
		/// </summary>
		public PageView() { }

		/// <summary>
		/// Plain view of a content page
		/// </summary>
		/// <param name="_page">Current content page</param>
		public PageView(ContentPage _page)
		{
			Page = _page;
		}

		/// <summary>
		/// Plain view with comments
		/// </summary>
		/// <param name="_page">Current content page</param>
		/// <param name="_comments">List of commments in the current page</param>
		public PageView(ContentPage _page, PagedList<ContentComment> _comments)
		{
			Page = _page;
			Comments = _comments;
		}

		/// <summary>
		/// If no comments or sub pages are present, then this is an index view
		/// </summary>
		/// <param name="_pages">List of pages in current index</param>
		public PageView(PagedList<ContentPage> _pages)
		{
			Pages = _pages;
		}

		/// <summary>
		/// Full content page view with sub page summaries
		/// </summary>
		/// <param name="_page">Current content page</param>
		/// <param name="_pages">Paged list of sub pages</param>
		/// <param name="_comments">Paged list of comments</param>
		public PageView(ContentPage _page, PagedList<PageSummary> _pages, 
			PagedList<ContentComment> _comments)
		{
			Page = _page;
			PageSummary = _pages;
			Comments = _comments;
		}

		/// <summary>
		/// Full content page view with expanded pages
		/// </summary>
		/// <param name="_page">Current content page</param>
		/// <param name="_pages">Paged list of sub pages</param>
		/// <param name="_comments">Paged list of comments</param>
		public PageView(ContentPage _page, PagedList<ContentPage> _pages, 
			PagedList<ContentComment> _comments)
		{
			Page = _page;
			Pages = _pages;
			Comments = _comments;
		}

		/// <summary>
		/// Checks if this view has a page to display. If not, then 
		/// it's probably a main index.
		/// </summary>
		public bool HasPage {
			get { return (Page != null); }
		}
    }
}

Our new PagesController

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using SimpleCMS.Models;
using SimpleCMS.Helpers;
using MarkdownSharp;

namespace SimpleCMS.Controllers
{
	public class PagesController : Controller
	{
		/// <summary>
		/// Page index view. Lists the pages under the given parent Id
		/// </summary>
		/// <param name="id">Page Id</param>
		/// <param name="page">Current page index</param>
		/// <returns>Page view</returns>
		public ActionResult Index(int? id, int? page)
		{
			id = id ?? 0;
			page = page ?? 1;

			ContentPage content = new ContentPage();
			PagedList<ContentPage> pages;

			using (SimpleCMSDataContext db = new SimpleCMSDataContext())
			{
				// Get the published pages
				pages = (from p in db.ContentPages
						 where p.ParentId == id.Value
						 orderby p.PubDate descending
						 select p).ToPagedList(page.Value, 10);

				if (id > 0)
				{
					content = (from p in db.ContentPages
							   where p.PageId == id.Value
							   select p).Single();
				}


			}

			PageView index = new PageView(pages);

			if (content.PageId > 0)
				index.Page = content;

			ViewData.Model = index;
			return View();
		}

		/// <summary>
		/// Basically the Details view
		/// </summary>
		/// <param name="id">Page Id</param>
		/// <param name="page">Current page index (for comments)</param>
		/// <returns>Read view or the index if no Id was found</returns>
		public ActionResult Read(int? id, int? page)
		{
			// Page Id
			id = id ?? 0;
			page = page ?? 1;

			if (id.Value > 0)
			{
				ContentPage content;
				PagedList<ContentComment> comments;

				using (SimpleCMSDataContext db = new SimpleCMSDataContext())
				{
					content = (from p in db.ContentPages
							   where p.PageId == id.Value
							   select p).Single();

					// Increment view count
					content.ViewCount = Util.DefaultInt(content.ViewCount, 0) + 1;

					comments = (from c in db.ContentComments
								where c.PageId == id.Value
								orderby c.CreatedDate ascending
								select c).ToPagedList(page.Value, 20);

					db.SubmitChanges();
				}

				ViewData.Model = new PageView(content, comments);
				return View();
			}
			return RedirectToAction("Index");
		}

		/// <summary>
		/// Creates a new page under the given parent Id
		/// </summary>
		/// <param name="id">Parent Id</param>
		/// <param name="collection">Form data input</param>
		/// <returns></returns>
		[HttpPost]
		public ActionResult Create(int? id, FormCollection collection)
		{
			// Parent Id
			id = id ?? 0;

			int newpageid = 0;

			ContentPage content;
			using (SimpleCMSDataContext db = new SimpleCMSDataContext())
			{
				// Gets the page data from form
				content = GetPageFromCollection(id.Value, collection, true);

				// Insert the new page
				db.ContentPages.InsertOnSubmit(content);

				if (id.Value > 0)
				{
					// Update parent page count if this page has a parent
					ContentPage parent = (from p in db.ContentPages
										  where p.PageId == id.Value
										  select p).Single();

					parent.PageCount = (from p in db.ContentPages
										where p.ParentId == id
										select p.PageId).Count() + 1;
				}

				// Save the page
				db.SubmitChanges();

				// Store the new Id for redirect
				newpageid = content.PageId;
			}

			// We have an Id
			if (newpageid > 0)
				return RedirectToAction("Read", new { id = newpageid });
			else
				return RedirectToAction("Index");
		}

		/// <summary>
		/// Edits a given page
		/// </summary>
		/// <param name="id">Page Id</param>
		/// <returns>Page edit view or the index if no Id is found</returns>
		public ActionResult Edit(int? id)
		{
			// Page Id
			id = id ?? 0;

			if (id.Value > 0)
			{
				using (SimpleCMSDataContext db = new SimpleCMSDataContext())
				{
					ContentPage content = (from p in db.ContentPages
										   where p.PageId == id.Value
										   select p).Single();

					ViewData.Model = content;
					return View();
				}
			}
			return RedirectToAction("Index");
		}

		/// <summary>
		/// Edits a given page
		/// </summary>
		/// <param name="id">Page Id</param>
		/// <param name="collection">Form data input</param>
		/// <returns>Page read view</returns>
		[HttpPost]
		public ActionResult Edit(int? id, FormCollection collection)
		{
			// Page Id
			id = id ?? 0;

			if (id.Value > 0)
			{
				using (SimpleCMSDataContext db = new SimpleCMSDataContext())
				{
					ContentPage content = (from p in db.ContentPages
										   where p.PageId == id.Value
										   select p).Single();

					ContentPage edited = GetPageFromCollection(id.Value, collection, false);

					// Basics
					content.Title = edited.Title;
					content.Description = edited.Description;
					content.AbstractText = edited.AbstractText;
					content.AbstractHtml = edited.AbstractHtml;
					content.BodyHtml = edited.BodyHtml;
					content.BodyText = edited.BodyText;

					//Edited date
					content.LastModified = edited.LastModified;

					// Moderation
					content.Approved = edited.Approved;
					content.AnonComments = edited.AnonComments;
					content.Moderated = edited.Moderated;

					db.SubmitChanges();
				}

				return RedirectToAction("Read", new { id = id });
			}
			return RedirectToAction("Index");
		}

		/// <summary>
		/// Deletes a given page
		/// </summary>
		/// <param name="id">Page Id</param>
		/// <returns>Delete page view or the index if no id is given</returns>
		public ActionResult Delete(int? id)
		{
			// Page Id
			id = id ?? 0;

			if (id.Value > 0)
			{
				ContentPage page;
				using (SimpleCMSDataContext db = new SimpleCMSDataContext())
				{
					page = (from p in db.ContentPages
							where p.PageId == id.Value
							select p).Single();
				}
				ViewData.Model = page;
				return View();
			}
			return RedirectToAction("Index");
		}

		/// <summary>
		/// Deletes a given page
		/// </summary>
		/// <param name="id">Page Id</param>
		/// <param name="collection">Form data input</param>
		/// <returns>Redirects to the page index</returns>
		[HttpPost]
		public ActionResult Delete(int? id, FormCollection collection)
		{
			id = id ?? 0;
			if (id.Value > 0)
			{
				try
				{
					using (SimpleCMSDataContext db = new SimpleCMSDataContext())
					{
						ContentPage page = (from p in db.ContentPages
											where p.PageId == id
											select p).Single();

						db.ContentPages.DeleteOnSubmit(page);
						db.SubmitChanges();
					}
				}
				catch { }
			}
			return RedirectToAction("Index");
		}


		/// <summary>
		/// Creates a page comment under the given page Id
		/// </summary>
		/// <param name="id">Page Id</param>
		/// <param name="collection">Form input data</param>
		/// <returns>Read page view</returns>
		[HttpPost]
		public ActionResult CreateComment(int? id, FormCollection collection)
		{
			// Page Id
			id = id ?? 0;

			if (id.Value > 0)
			{
				using (SimpleCMSDataContext db = new SimpleCMSDataContext())
				{
					// Get our page
					ContentPage content = (from p in db.ContentPages
										   where p.PageId == id
										   select p).Single();

					// If comments are enabled
					if (Util.DefaultBool(content.EnableComments, true))
					{
						// Create comment from the form data
						ContentComment comment =
							GetCommentFromCollection(id.Value, collection, true);

						// If the moderation is enabled, we need to 
						// disapprove this post first
						if(Util.DefaultBool(content.Moderated, false))
							comment.Approved = false;

						// Insert the new comment
						db.ContentComments.InsertOnSubmit(comment);

						// Update the comment count
						content.CommentCount = Util.DefaultInt(content.CommentCount, 0) + 1;

						db.SubmitChanges();
					}
				}
			}

			// There was no page or something went wrong
			if (id == 0)
				return RedirectToAction("Index");

			return RedirectToAction("Read", new { id = id.Value });
		}

		/// <summary>
		/// Edits a given comment
		/// </summary>
		/// <param name="id">Comment Id</param>
		/// <returns>Edit view</returns>
		public ActionResult EditComment(int? id)
		{
			// Comment Id
			id = id ?? 0;

			// Stores the page Id
			int pageid = 0;

			if (id.Value > 0)
			{
				ContentComment comment;
				ContentPage contentpage;
				using (SimpleCMSDataContext db = new SimpleCMSDataContext())
				{
					// Get the comment to be edited
					comment = (from c in db.ContentComments
							   where c.CommentId == id.Value
							   select c).Single();

					// Get the contnt page for this comment 
					// (we need some elements of this to show the comment)
					contentpage = (from p in db.ContentPages
								   where p.PageId == comment.PageId
								   select p).Single();

					// Attach the page to the comment
					comment.ContentPage = contentpage;

					pageid = comment.PageId;

					ViewData.Model = comment;
					return View();
				}
			}

			// There was no page or something went wrong
			if (pageid == 0)
				return RedirectToAction("Index");

			return RedirectToAction("Read", new { id = pageid });
		}


		/// <summary>
		/// Edits a given comment
		/// </summary>
		/// <param name="id">Comment Id</param>
		/// <param name="collection">Form data input</param>
		/// <returns>Page read view</returns>
		[HttpPost]
		public ActionResult EditComment(int? id, FormCollection collection)
		{
			// Comment Id
			id = id ?? 0;

			// Stores the page Id
			int pageid = 0;

			if (id.Value > 0)
			{
				ContentComment comment;
				ContentPage contentpage;
				using (SimpleCMSDataContext db = new SimpleCMSDataContext())
				{
					comment = (from c in db.ContentComments
							   where c.CommentId == id.Value
							   select c).Single();

					contentpage = (from p in db.ContentPages
								   where p.PageId == comment.PageId
								   select p).Single();

					ContentComment edited = GetCommentFromCollection(id.Value, collection, false);

					comment.Approved = edited.Approved;
					comment.Author = edited.Author;
					comment.AuthorEmail = edited.AuthorEmail;

					comment.BodyHtml = edited.BodyHtml;
					comment.BodyText = edited.BodyText;

					// Important to save changes here as the next step alters the model.
					db.SubmitChanges();

					comment.ContentPage = contentpage;

					pageid = comment.PageId;

					ViewData.Model = comment;
				}
			}

			// There was no page or something went wrong
			if (pageid == 0)
				return RedirectToAction("Index");

			return RedirectToAction("Read", new { id = pageid });
		}

		/// <summary>
		/// Deletes a given comment
		/// </summary>
		/// <param name="id">Comment Id</param>
		/// <returns>Delete comment view</returns>
		public ActionResult DeleteComment(int? id)
		{
			int pageid = 0;
			if (id.HasValue)
			{
				ContentComment comment;
				using (SimpleCMSDataContext db = new SimpleCMSDataContext())
				{
					comment = (from c in db.ContentComments
							   where c.CommentId == id.Value
							   select c).Single();

					pageid = comment.PageId;
				}
				ViewData.Model = comment;
				return View();
			}
			return RedirectToAction("Read", new { id = pageid });
		}

		/// <summary>
		/// Deletes a given comment
		/// </summary>
		/// <param name="id">Comment Id</param>
		/// <param name="collection">Form input data</param>
		/// <returns>Back to read page index</returns>
		[HttpPost]
		public ActionResult DeleteComment(int id, FormCollection collection)
		{
			try
			{
				int pageid = 0;
				using (SimpleCMSDataContext db = new SimpleCMSDataContext())
				{
					ContentComment comment = (from c in db.ContentComments
											  where c.CommentId == id
											  select c).Single();

					pageid = comment.PageId;
					db.ContentComments.DeleteOnSubmit(comment);
					db.SubmitChanges();
				}
				return RedirectToAction("Read", new { id = pageid });
			}
			catch { }
			return RedirectToAction("Index");
		}

		/// <summary>
		/// Gets a ContentPage object from the given form data
		/// </summary>
		/// <param name="id">Parent Id</param>
		/// <param name="collection">Form data input</param>
		/// <param name="newpage">True if creating a new page, false if editing</param>
		/// <returns>New ContentPage</returns>
		private ContentPage GetPageFromCollection(int id, FormCollection collection,
			bool newpage)
		{
			// Setup markdown
			Markdown m = new Markdown();

			// New page
			ContentPage content = new ContentPage();

			// Trigger date
			DateTime dt = DateTime.Now;

			// Basics
			content.ParentId = id;
			content.Title = Util.DefaultString(collection["title"], "No title");
			content.Description = Util.DefaultString(collection["description"], "");

			if (newpage)
			{
				content.Author = Util.DefaultString(collection["author"], "Anonymous"); // Just for now
				content.AuthorId = 0; // Just for now
			}

			// Page content
			content.AbstractText = Util.DefaultString(collection["abstracttext"], "");
			content.AbstractHtml = m.Transform(content.AbstractText);

			content.BodyText = Util.DefaultString(collection["bodytext"], "");
			content.BodyHtml = m.Transform(content.BodyText);


			// Eenable publishing
			content.Approved = Util.DefaultBool(collection["approved"], true);

			if (newpage)
			{
				// Nested
				content.ViewCount = 0;
				content.CommentCount = 0;
				content.PageCount = 0;

				// Times
				content.CreatedDate = dt;
			}

			// Pubdate
			content.PubDate = Util.DefaultDate(collection["pubdate"], dt);

			// Every time this function is called, we're doing something to the page.
			content.LastModified = dt;

			// Feedback
			content.EnableComments = Util.DefaultBool(collection["enablecomments"], true);
			content.AnonComments = Util.DefaultBool(collection["anoncomments"], true);
			content.Moderated = Util.DefaultBool(collection["moderated"], false);

			return content;
		}

		/// <summary>
		/// Gets a ContentComment object from the given form data
		/// </summary>
		/// <param name="id">Page Id</param>
		/// <param name="collection">Form data input</param>
		/// <param name="newpage">True if creating a new comment, false if editing</param>
		/// <returns>New ContentComment</returns>
		private ContentComment GetCommentFromCollection(int id, FormCollection collection,
			bool newcomment)
		{
			// Setup markdown
			Markdown m = new Markdown();

			// New comment
			ContentComment comment = new ContentComment();

			// Trigger date
			DateTime dt = DateTime.Now;


			// Author name
			comment.Author = Util.DefaultString(collection["author"], "Anonymous");

			if (newcomment)
			{
				// Basics
				comment.PageId = id;
				comment.AuthorId = 0; // Just for now
				comment.AuthorIP = Util.GetUserIP();
				comment.CreatedDate = dt;
			}

			comment.LastModified = dt;

			// We don't have an approval yet
			comment.Approved = true;
			comment.AuthorEmail = Util.DefaultString(collection["authoremail"], "");

			// Content
			comment.BodyText = Util.DefaultString(collection["bodytext"], "");
			comment.BodyHtml = m.Transform(comment.BodyText);

			return comment;
		}
	}
}

Onward to page 2…

Simple CMS with Linq to SQL part II

In our last installment, we looked at the basics of creating and deleting pages and comments. Now we’re going to dive into the views and editing portion including listing pages and comments in index (list) form and paging.

First, we can compile our project to make sure there no compile errors. If all went well, and if you used the mod portal modifications, you will be greeted with the following :

Welcome to your project

We’ll worry about the front page later. First let’s go into our PagesController and add a few views. I don’t want limit our flexibility with multiple full page views, so we’ll only have them for the Index, Read, Edit, Delete, EditComment and finally DeleteComment views. The rest will be strongly typed partial views (ViewUserControls).

Remember that the last time we didn’t include any programming to accomplish editing, but don’t worry. It’s fairly simple.

First, let’s create a control to display an index list of paged… er… content pages. You can create an empty control first called PageList, but we’ll need to modify the Inherits flag to match our PagedList class so we can access that model.

<%@ Control Language="C#" 
Inherits="System.Web.Mvc.ViewUserControl<PagedList<SimpleCMS.Models.ContentPage>>" %>
    <% foreach (var item in Model) { %>
    <div class="post">
           <h4><%: Html.ActionLink(item.Title, "Read", new { id = item.PageId }, 
                	new { title = Html.AltTitle(item.Title, item.Description) }) %></h4>

			<p class="data"><span><%: Html.FormalDate(item.PubDate) %> | 
			by <%: Html.AuthorLink(item.Author, item.AuthorId) %>.</span> 
			<span><%: Html.EditedDate(item.PubDate, item.LastModified) %>
			<%: Html.ActionLink("Edit", "Edit", new { id=item.PageId }) %> | 
			<%: Html.ActionLink("Delete", "Delete", new { id=item.PageId })%> | 
			<%: Html.CommentsLink(item.PageId, item.CommentCount) %> | 
			Viewed <%: item.ViewCount %> times</span></p>

                <% if (!String.IsNullOrEmpty(item.Abstract)) { %>
				<%= item.Abstract %>
				<%  } else { %>
				<%= item.BodyHtml %><% } %>			
    </div>
    <% } %>

Notice that there are multiple inheritences/implementations here. Our PagedList implements ContentPage, since that’s what we’re displaying in this control. Also note that we’re making use of a couple of helper functions, EditedDate and CommentsLink in the MvcHelpers class we created the last time.

Now we can view our pages, let’s also make another control that allows us to create them… Note, this has to be a strongly typed control called CreatePage that uses the ContentPage model.

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<SimpleCMS.Models.ContentPage>" %>
<div class="postform">
	<%
		using (Html.BeginForm("Create", "Pages", new { id = ViewData.Model.PageId }, FormMethod.Post))
		{%>
	<%: Html.ValidationSummary(true)%>
	<fieldset>
		<legend>Create new page</legend>
		<p>
			<label for="Title">Title</label>
			<%: Html.TextBoxFor(model => model.Title)%><%: Html.ValidationMessageFor(model => model.Title, "*")%></p>
		<p>
			<label for="Description">Description</label>
			<%: Html.TextBoxFor(model => model.Description)%>
		</p>
		<p>
			<label for="Abstract">Abstract</label>
			<%: Html.TextAreaFor(model => model.Abstract, new { rows = 5, cols = 60 })%>
		</p>
		<p>
			<label for="Author">Author</label>
			<%: Html.TextBoxFor(model => model.Author)%>
		</p>
		<p>
			<label for="BodyText">Content</label>
			<%: Html.TextAreaFor(model => model.BodyText, new { rows = 5, cols = 60 })%>
			<%: Html.ValidationMessageFor(model => model.BodyText)%>
		</p>
		<p>
			<label for="PubDate">Publish Date</label>
			<%: Html.TextBoxFor(model => model.PubDate)%>
		</p>
		<ul class="feedback options">
			<li><label><input type="checkbox" value="true" name="EnableComments" 
			<%= Html.CheckBoxChecked(ViewData.Model.EnableComments, true) %>/> Enable Comments</label></li>
			<li><label><input type="checkbox" value="true" name="AnonComments" 
			<%= Html.CheckBoxChecked(ViewData.Model.AnonComments, true) %>/> Anonymous Comments</label></li>
			<li><label><input type="checkbox" value="true" name="ModeratedComments" 
			<%= Html.CheckBoxChecked(ViewData.Model.Moderated, false) %>/> Moderated</label></li>
			<li><label><input type="checkbox" value="true" name="Approved" 
			<%= Html.CheckBoxChecked(ViewData.Model.Approved, true) %>/> Approved</label></li>
		</ul>
		<p>
			<input type="submit" value="Create new page" />
		</p>
	</fieldset>
</div>
<% } %>

Now we’re finally ready to create the Index View. Remember, this is going to be a full page strongly typed view, not just an .ascx. We just have to make sure that the strongly typed model is going to be PageView. You can leave View Content option as Empty since we’re going to be writing in our own functionality.

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" 
Inherits="System.Web.Mvc.ViewPage<SimpleCMS.Models.PageView>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
Index
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <h2>Index</h2>
	<% 		
		if (Model.Pages.Count > 0)
		{
			// We pages in this PageView, so we can start listing them.
			Html.RenderPartial("PageList", ViewData.Model.Pages);
		}
		if (Model.HasPage)
		{
			// We're adding a new page to an existing one.
			Html.RenderPartial("CreatePage", ViewData.Model.Page);
		}
		else
		{
			// We're creating a new content page from scratch
			Html.RenderPartial("CreatePage", new SimpleCMS.Models.ContentPage());
		}
		%>
</asp:Content>

See how uncluttered our page views are when we delegate all the “stuff” to user controls. Also note that we’re not fiddling with ViewData[“somestuff”] and such.

Go ahead and try it out.. You should be able to navigate to the /Pages/ section of your project after a compilation and create new pages.

Onward to the next step…

Simple CMS with Linq to SQL

A very basic content management system with multiple nested pages, comments and maybe a basic user management interface. Everything will be in ASP.Net MVC 2 in C#.

I’ve been meaning to do this for quite some time, but work and life kept getting in the way. I thought if I’m going to update for real, I’d at least post something useful.

I’ll be making use of my previous Linq to SQL Membership and Role providers for this to save time and effort. There are some minor alterations in the code, but those should be easy to make.

It would be helpful to have some prior knowledge of MVC basics, but not an absolute prerequisite. Things will go a bit smoother if you’re already familiar with it and Visual Web Developer Express, which my IDE of choice for this one. I’m also going to be using Sql Server Express 2008 and I won’t be adding an MDF to the App_Data folder as most quick examples would show, but create a database in SQL Server using the Management Studio instead. I think it’s far simpler to deploy to a hosted service with this starting point.

Because I’m going to be posting a lot of code in this, there is the potential for mistakes in my train of thought or during formatting. Please point them out if you happen to come across them or if you have improvements. But please keep in mind that this is a SimpleCMS, so we’re not going to get all that fancy ;)

Let’s start with a ContentPage table in your database :

Don't forget to set PageId as the primary key and set properties to auto increment.

Note that only three fields are required and one is by default because it’s the primary key. This is because I thought of having the most flexible arrangement program-wise without being concerned too much about required fields in the table. And because I wanted the CMS to double as a forum at some point… More on that later.

Now we need a ContentComments table :

Very important not to confuse AuthorId with AuthorIP. Seems silly to point that out, but it's happened to me ;)

Now we need to add a parent-child relationship by adding a foreign key to the ContentComments table. In this case, the PageId of the ContentComments table is going to be the foreign key to the ContentPages table’s PageId primary.

When adding relationships, it's better to keep the Primary and Foreign key field names the same.

Now back in your solution explorer, right click on the Models folder and Add > New Item. We’re going to add the Linq to SQL data classes here and call it SimpleCMS (you can call it any name of your choice, but just remember to keep it matching the project name for simplicity.

You’ll see the classes designer open with two sections exposed. You need to drag and drop the ContentPages and ContentComments tables into the class designer view.

Just remember to compile your project after adding these tables (or making any changes to the dbml) or else the next steps won't work.

Now that the database is set, we’re on our way to the fun part; the classes. But first, I’m going to break one of my own rules here and develop the template. This is because the default MVC theme is a bit too bare and, I believe, not very flexible even if you decide to hack it to pieces.

We’ll be using a version of the mod portal variation of one of my old template tutorials.

Onward to the next step…

Why are People so complicated?

That isn’t a rhetorical question; I really would like to know.

I ran into an old collegue of mine who had just accepted a gig creating your garden variety customer management app (basically a very light CRM and shopping cart). Normally she would have recommended using something off-the-shelf and customizing it rather than wasting time building something from scratch. But the back end was already so simple, and their need to change things in the front end was so frequent, that she needed to write it herself.

She was describing how she will implement the “Person” classes, when I felt a sudden wave of déjà vu as if I was transported back to a previous lifetime as a Soviet Scientist creating the N1 rocket. She was using the Table Profile Provider sample for most of the custom data fields, which would mean that adding a new field or property would have actually created a new column in the data table. The client would then augment the basic properties by editing the Web.config later on.

From the introduction :

The first sample provider (SqlTableProfileProvider) stores each Profile property in a separate database column. Furthermore the provider stores the Profile data without serializing it, which means that the Profile property type needs to be compatible with the target database column.

The second sample provider (SqlStoredProcedureProfileProvider) maps each Profile property to a parameter on a custom stored procedure. Like the table based provider, this provider expects that each Profile property is of a type that is compatible with its corresponding stored procedure parameter. The powerful aspect of the stored procedure based provider is that other than the requirement to implement some stored procedures with a specific set of parameters, you can implement whatever business logic you need in the stored procedures to map the Profile data to your own database schema and database logic.

OK, so she won’t know all the possible profile options the client would need, but there has to be some she just knows would be necessary. I had to ask her why she didn’t just map all possible combinations of profile properties into a couple of extra tables instead.

Now this isn’t the first time I a girl had given me the “wow, he’s a an idiot” look (I’ve lost count of those, really), but it was the first time in a long time, I felt it wasn’t deserved.

Why are People so complicated?

Two very important questions you need to ask when outlining a person in your app….

Which properties would a person have? And which of those would be more than one?

These two interdependent questions will really determine how you define people in your application since the rest is just fluff you can get away with by using the Provider Model for the most part.

  • A person may have several different street addresses for example. Maybe one for shipping and one for billing, and perhaps yet another for separate billing purposes or a separate shipping destination.
  • A person may have several different email addresses; one primary, one personal, one for work, one for spam etc…
  • A person may have several different phone numbers as well. One for daytime, one for evenings, one mobile, one fax etc…

Since as far back as I can remember, from the day I started programming, I’ve adhered to the age old philosophy : I’m a programmer = I’m lazy. But the good kind of lazy. The lazy that doesn’t want to hard-code properties in case they need to be changed. The lazy that doesn’t re-invent the wheel, but only wants to add better rims.

I’ve never believed that it’s prudent nor practical to access or store complex profile properties using the Profile Provider. While this might seem ironic, you also need to consider that it doesn’t have native knowlege of how you’re storing the data. That how is what should determine whether the Profile Provider is appropriate.

A little complexity now will save more of it later on

Consider the following model…

Membership and Profile model

This is an example layout from the Linq to SQL class using the database I created for the OPAR project. I’ll the other tables and matching classes at a later date.

Instead of creating a new column for each profile property, you essentially implement a property bag per member. The MemberProfileField stores all your data with one entry per field. The drawback is that your MemberProfileFields table will get quite large over time, however you’re not altering the schema in any way which means all your content will still be safe. Which brings me to another programming philosophy…

Try to keep your data out of your provider

Tell your data what type it’s storing but nothing else; In this case, I have a field in MemberProfileFields called “FieldType”. This isn’t int, bool, xml etc… It’s actually textbox, multiline, option and so on. You’re creating a web application, so store what input type you’re using to get and store the data. This is very handy for the future as you can add new HTML form types at a later date. The provider logic dictates which field it will tap to store the data.

And back in your profile, you can have a helper class with the following mappings :

text = <input type=”text” id=”{FieldName}” name=”{FieldName}” value=”{ContentText}” />
multiline = <textarea id=”{FieldName}” name=”{FieldName}”>{ContentLongText}</textarea>
option = <input type=”checkbox” id=”{FieldName}” name=”{FieldName}” value=”{ContentBool}” />
… And so on.

Of course, you can do one better by adding a ProfileFieldTypes table to extend this a lot further and substitute FieldType for FieldTypeId with a matching foreign key as well. By branching out the schema to tables, you can actually dynamically create profile properties without ever having to touch the Web.config .