Simple CMS with Linq to SQL

Now we really start cranking out the classes.

Create a new folder in your solution explorer called Lib and in this folder create another one called Helpers.

The Helpers folder will have all our neat formatting as well as the presentation widgets. One of the most useful of these is called the PagedList. You’ve probably seen many different variations of this class, but I’m using here the bare essentials needed to get the job done.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace System.Web.Mvc
{
	public interface IPagedList
	{
		int TotalCount { get; set; }
		int PageCount { get; set; }
		int PageSize { get; set; }
		int PageIndex { get; set; }

		bool HasPreviousPage { get; }
		bool HasNextPage { get; }
	}

	public class PagedList<T> : List<T>, IPagedList, IEnumerable
	{
		public int TotalCount { get; set; }
		public int PageCount { get; set; }
		public int PageSize { get; set; }
		public int PageIndex { get; set; }

		public bool HasPreviousPage
		{
			get { return PageIndex > 1; }
		}
		public bool HasNextPage
		{
			get { return (PageIndex * PageSize) <= TotalCount - 1; }
		}
		
		public bool IsPreviousPage
		{
			get { return PageIndex <= 0; }
		}

		public bool IsLastPage
		{
			get { return PageIndex >= (PageCount - 1); }
		}

		public PagedList(IEnumerable<T> source, int index, int pageSize)
		{
			index = ((index - 1) < 0) ? 0 : (index - 1);

			this.TotalCount = source.Count();
			this.PageSize = pageSize;
			this.PageIndex = index + 1;

			if (this.TotalCount > 0)
			{
				if (index == 0)
					this.AddRange(source.Take(pageSize).ToList());
				else
					this.AddRange(source.Skip(index * pageSize).Take(pageSize).ToList());

				this.PageCount = (int)Math.Ceiling(this.TotalCount / (double)this.PageSize);
			}
			else
				this.PageCount = 0;
		}

		public PagedList(List<T> source, int index, int pageSize)
		{
			index = ((index - 1) < 0) ? 0 : (index - 1);

			this.TotalCount = source.Count();
			this.PageSize = pageSize;
			this.PageIndex = index + 1;

			if (this.TotalCount > 0)
			{
				if (index == 0)
					this.AddRange(source.Take(pageSize).ToList());
				else
					this.AddRange(source.Skip(index * pageSize).Take(pageSize).ToList());

				this.PageCount = (int)Math.Ceiling(this.TotalCount / (double)this.PageSize);
			}
			else
				this.PageCount = 0;
        }
	}

	public static class Pagination
	{
		public static PagedList<T> ToPagedList<T>(this IQueryable<T> source, int index, int pageSize)
		{
			return new PagedList<T>(source, index, pageSize);
		}

		public static PagedList<T> ToPagedList<T>(this IQueryable<T> source, int index)
		{
			return new PagedList<T>(source, index, 10);
		}
	}
}

Over the last few months, this class in particular has saved me quite a lot of work and repetition.

We now add another helper class with little niceties that make our V in our MVC more pleasant. You may notice that I reused my friendly dates function from some time back. I just loathe the idea of using unformatted dates with plain backslashes.

using System;
using System.Web;
using System.Web.Mvc;
using System.Text;
using SimpleCMS.Helpers;

namespace System.Web.Mvc.Html
{
	public static class MvcHelpers
	{
		/// <summary>
		/// Displays the last edited date
		/// </summary>
		public static string EditedDate(this HtmlHelper helper, Object ddt, Object ddm)
		{
			if ((DateTime)ddt != (DateTime)ddm)
				return "Edited " + MvcHelpers.FriendlyDate(helper, ddm) + " | ";

			return "";
		}

		/// <summary>
		/// Direct link to the comments section of each page
		/// </summary>
		public static MvcHtmlString CommentsLink(this HtmlHelper helper, int pageid, int? count)
		{
			if (count.HasValue && count.Value > 0)
				return helper.ActionLink(count.Value + " Comments", "Read", "Pages", null, null, "comments", new { id = pageid }, null);

			return helper.ActionLink("No Comments", "Read", "Pages", null, null, "comments", new { id = pageid }, null);
		}

		/// <summary>
		/// Formatted link to the author's profile page
		/// </summary>
		public static MvcHtmlString AuthorLink(this HtmlHelper helper, string a, int? id)
		{
			if (id.HasValue && id.Value > 0)
				return helper.ActionLink(a, "Profile", "Users", new { id = id.Value }, new { });

			return helper.ActionLink(a, "Profile", "Users", new { id = "Anonymous" }, new { });
		}

		/// <summary>
		/// Title descriptions for links
		/// </summary>
		public static string AltTitle(this HtmlHelper helper, string t, string d)
		{
			return (string.IsNullOrEmpty(d)) ? t : d;
		}

		/// <summary>
		/// Evaluates the default value and provides checked checkbox
		/// </summary>
		public static string CheckBoxChecked(this HtmlHelper helper, bool? val, bool d)
		{
			if (SimpleCMS.Helpers.Util.DefaultBool(val, d))
				return "checked=\"checked\" ";

			return "";

		}

		/// <summary>
		/// Shows the long Sunday, October 10, 2010 date format.
		/// </summary>
		public static string FormalDate(this HtmlHelper helper, Object ddt)
		{
			DateTime dt = (DateTime)ddt;
			return dt.ToLongDateString();
		}

		/// <summary>
		/// Creates a formatted pager to navigate a PagedList
		/// </summary>
		/// <param name="helper">This required to tie into the Html Helpers</param>
		/// <param name="plist">A list of "stuff" to page.</param>
		/// <param name="url">Current url without the page. E.G. /Pages/Read/1/###</param>
		/// <returns>Formatted pager</returns>
		public static string Pager(this HtmlHelper helper, IPagedList plist, string url)
		{
			StringBuilder sb = new StringBuilder();

			// This is the convention : 
			// <h5>Page : <strong class="curr">1</strong> . <a href="">2</a></h5>

			// No need to show pager if we have less than one page
			if (plist.PageCount > plist.PageSize)
			{
				// Begin the pager
				sb.Append("<h5> Page : ");

				if (plist.HasPreviousPage)
					sb.Append("<a href=\"" + url.Replace("###", 
						(plist.PageIndex - 1).ToString()) + "\">&lt;</a> ");

				// Adding the rest of the links
				for (int i = 1; i < plist.PageCount + 1; i++)
				{
					// Current page doesn't need a link
					if (i == plist.PageIndex)
					{
						sb.Append("<strong class=\"curr\">");
						sb.Append((i).ToString());
						sb.Append("</strong> . ");
					}
					else
					{
						sb.Append("<a href=\"");
						sb.Append(url.Replace("###", (i).ToString()));
						sb.Append("\">" + (i) + "</a> . ");
					}
				}

				if (plist.HasNextPage)
					sb.Append("<a href=\"" + url.Replace("###", 
						(plist.PageIndex + 1).ToString()) + "\">&gt;</a>");

				sb.Append("</h5>");
			}
			return sb.ToString();
		}

		/// <summary>
		/// A more user friendly X minutes/hours/days(etc..) ago date format
		/// </summary>
		public static string FriendlyDate(this HtmlHelper helper, Object ddt)
		{
			// Get all the variables set
			DateTime dt = (DateTime)ddt;
			DateTime now = DateTime.Now;
			StringBuilder sb = new StringBuilder();
			TimeSpan elapsed;

			int days = 0;
			int hours = 0;
			int minutes = 0;
			int years = 0;

			// If the given date is *before* now
			if (now > dt)
				elapsed = now.Subtract(dt);
			// If the given date is *after* now
			else
				elapsed = dt.Subtract(now);

			// Leap years are always fun...
			// I don't care too much about accuracy after the current year
			if (DateTime.IsLeapYear(dt.Year))
				years = (int)(elapsed.Days / 366);
			else
				years = (int)(elapsed.Days / 365);

			days = (int)elapsed.TotalDays;
			hours = (int)elapsed.TotalHours;
			minutes = (int)elapsed.TotalMinutes;

			// There aren't any hours days or years yet
			if (years <= 0 && days <= 0 && hours <= 0)
			{
				if (minutes > 0)
				{
					sb.Append(minutes.ToString("0") + " minute");

					if (minutes > 1)
						sb.Append("s");
				}
				else
				{
					// We're under a minute
					sb.Append("A few seconds");
				}
			}
			else
			{
				// If we have years, then days and hours don't really mean much
				if (years > 0)
				{
					if (years < 10)
					{
						sb.Append(years.ToString("0") + " year");
						if (years > 1)
							sb.Append("s");
					}
					// ... Hey, ASP lasted quite a while. You just never know.
					else if (years > 10 && years < 30)
					{
						int dec = years / 10;
						sb.Append(dec + " decade");
						if (dec > 1)
							sb.Append("s");
					}
					else
					{
						sb.Append("A very long time");
					}
				}
				else
				{
					// We have days and hours
					if (days > 0)
					{
						sb.Append(days.ToString("0") + " day");
						if (days > 1)
							sb.Append("s");
					}
					if (hours > 0)
					{
						sb.Append(" " + hours.ToString("0") + " hour");
						if (hours > 1)
							sb.Append("s");
					}
				}
			}

			if (now > dt)
				// Past time
				sb.Append(" ago");
			else
				// Future time
				sb.Append(" from now");

			return sb.ToString();
		}
	}
}

Now the niceties are complete, we can move on to adding our PageView.

A lot of tutorials overlook the fact that you’re actually sending a model to the page. Hence the Model, View, Controller. Well, it doesn’t make much sense to pass a whole lot of information through the ViewState array alone.

So we’re going to make a specific PageView class that does exactly that. It’s a model of the requested view that is strongly typed and easy to access.

Back in your solution explorer, right click the Models folder and select Add > New Item. We’re going to add a new code file which will be our PageView class.

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 or sub pages.
		/// </summary>
		public PagedList<ContentPage> Pages { get; set; }

		/// <summary>
		/// Paged list of comments in the current page
		/// </summary>
		public PagedList<ContentComment> Comments { 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 comments and sub 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); }
		}
    }
}

We’ll get to the breadcrumbs a little later. You could pretty much call this a factory pattern, but I don’t believe in absolutes so we’ll just call it a model.

Onward to step 4…

8 thoughts on “Simple CMS with Linq to SQL

  1. Pingback: Simple CMS with Linq to SQL part II « This page intentionally left ugly

  2. Pingback: Forum Tables « This page intentionally left ugly

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s