Giving up on ASP.Net

Ever get really, really, really excited about a new puzzle you got as a present, spend hours carefully trying to put the it together only to realise after you’ve glued the last piece in, that [ship, tower, submarine, UFO] you were so proud of a second ago now looks like a sex toy that you’d be embarrassed to show anyone?

This isn’t an easy realisation to have and frankly, it can make you a bit ill. It’s not really your fault, the puzzle is just a crappy design and no one at the company that makes it bothered to build the thing, take a step back and go “yup, that’s a dildo; let’s not sell it to children”.

I’ve had the very same realization at work after years of working with ASP.Net and recently, MVC.

For the past few months, I’ve been working on projects on my own for the first time in almost a year. No team, no consultants, no “UI experts”. Just me, a goal and a deadline. I’ve been delivering on time and everyone’s been happy, except me.

On each project I’ve delivered, I got the sinking feeling I’ve just sold the Titanic. It’s very pretty, usually humongous and it will fail as a product.

The problem isn’t the product itself, it’s the platform its built on, the dangerous climate and slow response at the wheelhouse. It will crumble a year from now with the changing tides and needs and we’ll be using some entirely new platform (probably smaller) to build again.

Let’s check out the MVC thing

It’s the latest and greatest “in” thing for ASP.Net, looks ostensibly very useful and shiny, therefore it must be good.

But it’s completely pointless.

You end up writing more code to accomplish the same things than, say PHP or Django with Python, and all the added functionality you create to make it more usable, just adds more bloat.

The overriding issue with most MS products are premature obsolescence, needless complexity and breaking changes (I left out security since this is a blog post and not War and Peace) and I’ve only manged to stay ahead by constantly keeping on top of changing frameworks, functionality, release updates and even breaking changes that drove everyone nuts. This is even more obvious when developing for the web.

I find myself adding more “stuff” just to make things easier for everyone, thereby having to teach this new “stuff” to the folks inheriting the project which in turn makes everything harder for everyone.

In the past couple of years, I’ve had to learn all I can about MVC 2 and 3, Razor, .Net 4.0 and I’m thinking to myself a lot of the problems I come across are…

Problems that shouldn’t exist

Developing in ASP.Net eventually leads to needless complexity and fragmentation.

There are magnificent projects built on the platform and I can’t help but wonder how even more fantastic (and maintainable) they would be if they had been built on something else.

I learned a lot about developing for the web while working alone. Developing with a team is often less productive, not more, and I caught the first hints of this as the economy shrank along with our team sizes. The best work I’ve seen anyone do was when they were at it alone, even with a ridiculous deadline.

The vast majority of ASP.Net development is geared toward the team, hence the fragmentation. There’s no real sense of why we need to split a project into different segments to work on other than the fact that’s just the way it’s always been done and that’s the end of it. The end result is that there’s a culture of adding or otherwise handing off functionality to third-party projects on features that you don’t need, shouldn’t need and should never need.

A small example of this dangerous interdependency was when MS killed AntiXSS (a product that is unnecessary if your app is well designed) with no warning and then it decided to hold you over the bonfire that was your project now that you were already shafted.

Unless you’re using a third-party project in its entirety (after vetting the thing for holes) you’re better off creating a simpler alternative from scratch a lot of the time. If the problem you’re trying to solve is complex, ask yourself, is it really the problem that’s complex or just the solution you created for it.

Methodology shouldn’t trump getin’ Sh!% done

You want it to work and make money.

That’s it… that’s the only reason anyone invests in a commercial product. So what’s the sense sticking to dogma when it comes to platform? Brand loyalty makes sense if it’s helpful and conducive to productivity, personal satisfaction and imagination. The last two are just as important as budget and timeline despite what your boss might say because that’s where the best solutions come from.

Just for the record, I’m not the only one who came to this realization as Milan Negovan also did before. I’m just a couple of years late.

Let’s set aside the complexity. Even the cost doesn’t warrant going with ASP.Net for the vast majority of the work I’ve done and this isn’t just about hosting and service availability. With more demand you will spend exponentially more on licensing and server resources. Microsoft hasn’t delivered a product that will allow you to do more with less with regard to cost when it comes to developing on the web.

What does it mean?

I can’t in good conscience recommend ASP.Net for new projects.

If you have heavy dependencies on Microsoft products, then chances are, you don’t have a choice in the matter. But if the project is brand new and you have no ball and chain, go with something else. I can’t force you to do this so if you insist on nothing else, I guess I’ll still use it (after all, I need cash to build my cabin).

Considering how much code I’ve posted on this blog pertaining to ASP.Net and C#, this may seem hypocritical, but you have to realize, sometimes you do need to take your time on the throne and finish that big dump to feel relief.

Autocomplete with jQuery and MVC

This is just a prelude to a complete spellcheck addon to the discussion forum. I figured I’d start with basic autocomplete first that ties into the wordlist.

All spellcheckers essentially refer to a global wordlist in the specified language and any words that don’t belong, get flagged.

The hardest part of this turned out to be finding a decent wordlist. I was actually surprised at the delicate balance between finding a “good enough” list and one that’s “too good”. Too good? Yes, apparently a list that has too many words will mean you will get a lot of misses where an apparent misspelling turned out to be an obscure word… and you didn’t mean to use obscure words.

The final list I settled on has a word count of 125,346 and was from the Ispell project which also has common acronyms. Note: This is not the same as Iespell (written ieSpell), although if you Google, “Ispell”, you’ll get “ieSpell as the first result. Ispell lists are available for download at the Kevin’s Wordlist page. I have also combined the 4 main english lists into one file (MS Word). WordPress, strangely, won’t allow plain text files to be uploaded, but allows richtext documents. Email me if you want the plaintext version.

I started with a simple DB table to store all the entries. Since I may also be adding more languages, I also have a WordLang field which can be something small like “en”, “de”, “fr” etc…

Wordentries table

 

I then created an MVC app and loaded each of the wordlist files into the db using a simple function (this can take a while depending on filesize):

public List GetWords(string p) {
	var query = from line in File.ReadAllLines(p)
			select new Wordentry
			{
				WordText = NormalizeString(line),
				WordLowercase = NormalizeString(line).ToLower(),
				WordLang = "en"
			};
	return query.ToList();
}

 

After feeding it a HostingEnvironment.MapPath to the filename, I can use this to load all entries into the list and call a db.Wordentries.InsertAllOnSubmition the result. NormalizeString is another helper function which I will list below.

I’m using a Spellword model instead of directly using the Wordentry object since I may want to extend the returned result in the future and changing the columns in the DB wouldn’t be practical.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace Spellcheck.Models
{
	public class Spellword
	{
		public int Id { get; set; }
		public string Spelling { get; set; }
		public string Lowercase { get; set; }
		public string Lang { get; set; }
	}
}

 

And we’re using a SpellRepository class so we’ll keep the controllers free of too much data access stuff.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.IO;
using System.Text;
using System.Globalization;

namespace Spellcheck.Models
{
	public class SpellRepository
	{
		// DataContext global
		private readonly CMDataContext db;

		public SpellRepository(CMDataContext _db)
		{
			db = _db;
		}

		/// <summary>
		/// Counts the total number of word entries
		/// </summary>
		/// <returns>Wordcount int</returns>
		public int GetCount()
		{
			return (from w in db.Wordentries
			 select w.WordText).Count();
		}

		/// <summary>
		/// Searches a given word or word fragment
		/// </summary>
		/// <param name="word">Search word/fragment</param>
		/// <param name="word">Number of returned results</param>
		/// <param name="word">Language to search. Defaults to 10</param>
		/// <param name="word">Search lowercase field only</param>
		/// <returns>List of spellwords</returns>
		public List<Spellword> GetWords(string word, int limit = 10,
			string lang = "en", bool lower = true)
		{
			word = (lower) ?
				NormalizeString(word.ToLower()) :
				NormalizeString(word);

			var query = from w in db.Wordentries
						select w;

			// Get only unique entries in case we have
			// duplicates in the db (Edited from an earlier "GroupBy")
			query = query.Distinct().OrderBy(w => w.WordLowercase);

			// If a language code was specified
			if (!string.IsNullOrEmpty(lang))
				query = query.Where(w=>w.WordLang == lang);

			// Lowercase?
			query = (lower) ?
				query.Where(w => w.WordLowercase.StartsWith(word)) :
				query.Where(w => w.WordText.StartsWith(word));

			// Order alphabetically
			query = query.OrderBy(w => w.WordLowercase);

			return (from w in query
					select new Spellword
					{
						Id = w.WordId,
						Spelling = w.WordText,
						Lowercase = w.WordLowercase,
						Lang = w.WordLang
					}).Take(limit).ToList();
		}
		/// <summary> 
		/// Inserts a new list of words into the spellcheck library
		/// </summary>
		public void SaveWords(List Words)
		{
			var query = Words.GroupBy(w => w.Spelling)
				.Select(w => w.First())
				.OrderBy(w => w.Spelling).ToList();

			List Entries = (from w in query
									   orderby w.Spelling ascending
									   select new Wordentry
									   {
										   WordText = w.Spelling,
										   WordLowercase = w.Lowercase,
										   WordLang = w.Lang
									   }).ToList();

			db.Wordentries.InsertAllOnSubmit(Entries);
			db.SubmitChanges();
		}

		/// <summary> 
		/// Helper function normalizes a given word to the Unicode equivalent
		/// </summary>
		/// <param name="txt">Raw word</param>
		/// <returns>Normalized word</returns>
		private static string NormalizeString(string txt)
		{
			if (!String.IsNullOrEmpty(txt))
				txt = txt.Normalize(NormalizationForm.FormD);

			StringBuilder sb = new StringBuilder();

			sb.Append(
				txt.Normalize(NormalizationForm.FormD).Where(
					c => CharUnicodeInfo.GetUnicodeCategory(c)
					!= UnicodeCategory.NonSpacingMark).ToArray()
				);

			return sb.ToString().Normalize(NormalizationForm.FormD);
		}
	}
}

To use this, we’ll just add a JsonResult action to our controller. I just created a Suggestions action in the default Home controller since this is just an example.

public JsonResult Suggestions(string word, int limit = 10, string lang="en")
{
	List Words = new List();
	if (!string.IsNullOrEmpty(word))
	{
		using (CMDataContext db = new CMDataContext())
		{
			SpellRepository repository = new SpellRepository(db);
			// 10 results is usually enough
			Words = repository.GetWords(word, limit, lang);
		}
	}
	// Need to use AllowGet or else, we'll need use POST
	return Json(Words, JsonRequestBehavior.AllowGet);
}

 

… And that pretty much covers the backend for now.

To test out to see if the word suggestion works, we’ll do one autocomplete textbox. Just add the jQuery and jQuery UI script files and include the jQuery UI CSS to your layout first and add this to the default view :

<script type="text/javascript">
	$(function () {
		var searchtext = $("#search");
		searchtext.autocomplete({
			source: function (request, response) {
				$.ajax({
					url: "/Home/Suggestions", // Or your controller
					dataType: "json",
					data: { word: request.term },
					success: function (data) {
						// Returned data follows the Spellword model
						response($.map(data, function (item) {
							return {
								id: item.Id,
								label: item.Spelling,
								value: item.Lowercase
							}
						}))
					}
				});
			},
			minlength: 3
		});
	});
</script>
<form action="/" method="post">
<input id="search" type="text" name="search" />
</form>

 

Fun fact : Total misspellings as I was writing this (excluding Ispell/ieSpell names and code) before running spellcheck = 12.

Yeah, I really can’t spell.

ASP.Net MVC 3 Script license WTF?!

I had a few breaks between visits so I decided to re-write some of my old work in MVC 3 and Razor. I was going through all the included files in the Scripts folder when I came across Modernizr. I admit that I haven’t really looked into it that much since most of my client work involves CSS2 and XHTML, not CSS3 and HTML5. And the few times I needed HTML5 compatibility for <video> and such it was included as needed.

For the record, Modernizr is offered under a BSD/MIT license.

Imagine my surprise when, in Visual Web Developer Express 2010, I opened up modernizr-1.7.js to find this on top :

/*!
* Note: While Microsoft is not the author of this file, Microsoft is
* offering you a license subject to the terms of the Microsoft Software
* License Terms for Microsoft ASP.NET Model View Controller 3.
* Microsoft reserves all other rights. The notices below are provided
* for informational purposes only and are not the license terms under
* which Microsoft distributed this file.
*
* Modernizr v1.7
* http://www.modernizr.com
*
* Developed by:
* - Faruk Ates  http://farukat.es/
* - Paul Irish  http://paulirish.com/
*
* Copyright (c) 2009-2011
*/

After that, I looked into the rest of the included script files and I found the same in jQuery and jQuery UI as well and they are originally MIT/GPL . Now I’ve either been living under a rock (actually living “out-of-town” for a while) or this is a sudden inclusion not present in any pre-MVC3 projects because I don’t recall seeing anything like this before.

So Microsoft is re-licensing the original code because it is included with an MVC 3 project template? Can they even do that even though the files are included in their project template (what about consent form the original authors)? Is this only because it was included in the template or does it also apply when I use the original code instead of the provided copies as long as I’m still using it with other Microsoft provided script files?

I really don’t like seeing surprises like this because it’s not a technical problem; It’s a legalese problem and I really hate to have to choose code based on licenses. I’m now tempted to simply delete the entire contents of the Scripts folder and download everything from scratch.

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…