Building a photoblog with Nancy and Simple.Data Part 6: Adding comments

This is the sixth part in my Building a photoblog with Nancy and Simple.Data series:

  1. Setting up the project
  2. Defining the routes
  3. Rendering some views
  4. Adding the database
  5. Updating Simple.Data
  6. Adding comments
  7. The archives

It’s been a while since my last post and a lot of things have changed since then. In this post we’ll update Simple.Data to version 0.6.7 and Nancy to 0.6. Then we’ll see what we need to do to finish the page that displays a photo. Let’s get cracking!

Updating Simple.Data

This is probably the easiest part. Just like before, open the Package Manager Console and enter Update-Package Simple.Data.SqlCompact40. This will update all three Simple.Data packages to their latest version. At the moment that I’m writing this, that’s version 0.6.7.

Updating Nancy

Remember how we added the different Nancy assemblies manually because not all parts were available on Nuget? Well, that’s changed and the release of Nancy 0.6 seems like a good occasion to remove the manually added assemblies and use Nuget to get the latest version of Nancy!

In the Solution Explorer, right click Nancy and choose Remove. Repeat this for Nancy.Hosting.Aspnet and Nancy.ViewEngines.Razor. Delete the three dll-files from the Lib directory in the solution directory as well.

Open the Package Manager Console and enter Install-Package Nancy. When Nancy has been added, use Install-Package Nancy.ViewEngines.Razor and Install-Package Nancy.Hosting.Aspnet to add the rest.

When you rebuild your project, everything should be working just fine. That wasn’t too hard, was it?

Update the PhotoModule

Let’s update the Get["/{slug}"] route so that it displays the requested photo and provides links to the next and previous photo. Because of the amazing work of Mark Rendle on Simple.Data, this is extremely easy:

Get["/{slug}"] = parameters =>
{
	string slug = (string)parameters.slug;
	Models.Photo photo = DB.Photos.FindBySlug(slug);

	if (photo == null)
	{
		// No photo found with this slug, we'll just redirect to the homepage
		return Response.AsRedirect("/");
	}
	else
	{
		var model = new Models.PhotoDetail();
		model.Photo = photo;

		model.PreviousSlug = DB.Photos
		                       .Query()
							   .Select(DB.Photos.Slug)
							   .Where(DB.Photos.Published == true && DB.Photos.DatePublished < photo.DatePublished.Value)
							   .OrderByDatePublishedDescending()
							   .Take(1)
							   .ToScalarOrDefault<string>();

		model.NextSlug = DB.Photos
		                   .Query()
						   .Select(DB.Photos.Slug)
						   .Where(DB.Photos.Published == true && DB.Photos.DatePublished > photo.DatePublished.Value)
						   .OrderByDatePublished()
						   .Take(1)
						   .ToScalarOrDefault<string>();

		return View["photodetail", model];
	}
};

How neat is that? First we select the requested photo, based on the slug. Next we select the slug of previous and next photo’s, put them in the model and let Nancy pass it on to the photodetail.cshtml view. That’s the same view we use for the homepage.

Adding comments

Now that we can browse the photo’s, let’s see how we can give visitors the possibility to add comments.

The Comment class

We already have a table in the database to store the comments. We also need a class that corresponds to that table. Add a new class Comment to the Models directory and give it the following code:

public class Comment
{
	public int Id { get; set; }
	public string Name { get; set; }
	public string Email { get; set; }
	public string Website { get; set; }
	public string Message { get; set; }
	public int PhotoId { get; set; }
	public bool Approved { get; set; }

	public bool IsValid()
	{
		if (String.IsNullOrWhiteSpace(Name)) return false;
		if (String.IsNullOrWhiteSpace(Email))
		{
			return false;
		}
		else
		{
			bool validAddress = true;
			try
			{
				var address = new System.Net.Mail.MailAddress(Email).Address;
			}
			catch (FormatException)
			{
				validAddress = false;
			}

			if (!validAddress) return false;
		}

		if (String.IsNullOrWhiteSpace(Message))
		{
			return false;
		}
		else
		{
			Uri uri;
			if (!System.Uri.TryCreate(Website, UriKind.Absolute, out uri)) return false;
		}

		return true;
	}
}

As you can see we have a property for each column in the database table. They have the same name so Simple.Data can map them correctly. I have also added a method called IsValid() to the Comment class. In a larger application you probably want to use a separate validator class, but in this case we’ll just stick with this approach.

Updating our PhotoDetail view model

Since the comments of a photo need to be passed to the view, we need to add some code to the PhotoDetail class:

public class PhotoDetail
{
	public Photo Photo { get; set; }

	public string NextSlug { get; set; }
	public bool HasNext { get { return !String.IsNullOrEmpty(this.NextSlug); } }

	public string PreviousSlug { get; set; }
	public bool HasPrevious { get { return !String.IsNullOrEmpty(this.PreviousSlug); } }

	public string ErrorMessage { get; set; }
	public bool HasErrorMessage { get { return !String.IsNullOrEmpty(this.ErrorMessage); } }

	public List<Comment> Comments { get; set; }

	public PhotoDetail()
	{
		Comments = new List<Comment>();
	}
}

Next to the comments (line 14), I have also added an ErrorMessage property (lines 11 and 12). This will enable us to pass an error message to the view when a user enters incorrect information while adding a comment.

Updating the view

To display the list of comments and a form to add a new comment, I have added this code to the photodetail.cshtml file (right below the </nav>-tag):

<div id="comments">
	<h2>Comments</h2>
	<div id="commentsform">
		<a name="comments"></a>

		@foreach (var comment in @Model.Comments)
		{
			<div class="comment">
				<div class="commenter">
					@if (String.IsNullOrEmpty(comment.Website))
					{
						@comment.Name <text> wrote:</text>
					}
					else
					{
						<a href="@comment.Website">@comment.Name</a> <text> wrote:</text>
					}
				</div>
				<div class="message">@comment.Message</div>
			</div>
		}

		<h3>Add a comment</h3>
		<form action="/photo/@Model.Photo.Slug/addcomment" method="POST">
			@if (@Model.HasErrorMessage)
			{
				<div id="errormessage">
					@Model.ErrorMessage
				</div>
			}
			<div class="formitem">
				<label for="Name">Your name</label>
				<input type="text" id="Name" name="Name" />
				<span class="mandatory">*</span>
			</div>

			<div class="formitem">
				<label for="Email">Your email address</label>
				<input type="text" id="Email" name="Email" />
				<span class="mandatory">*</span>
			</div>

			<div class="formitem">
				<label for="Website">Your website</label>
				<input type="text" id="Website" name="Website" />
			</div>

			<div class="formitem">
				<label for="Message">Your message</label>
				<textarea id="Message" name="Message"></textarea>
				<span class="mandatory">*</span>
			</div>

			<div class="formitem">
				<input type="submit" id="commentsubmit" name="commentsubmit" value="Submit" />
			</div>
		</form>
	</div>
</div>

Notice how the four input elements have the same name as the properties of our Comment class. We’ll see why this is important in a moment. As you can see on line 24, the form will post to /photo/{slug}/addcomment. This route is defined in our PhotoModule. Let’s update it!

Adding a comment

Here’s the new code for the Post["/{slug}/addcomment"] route:

Post["/{slug}/addcomment"] = parameters =>
{
	string photoSlug = (string)parameters.slug;

	int? photoId = DB.Photos
	                 .Query()
					 .Select(DB.Photos.Id)
					 .Where(DB.Photos.Slug == photoSlug)
					 .ToScalarOrDefault<int?>();

	if (photoId.HasValue)
	{
		Models.Comment comment = this.Bind<Models.Comment>("Id", "PhotoId", "Approved");
		comment.PhotoId = photoId.Value;
		comment.Approved = true;

		if (comment.IsValid())
		{
			DB.Comments.Insert(comment);
		}
		else
		{
			Session["commenterror"] = true;
		}

		return Response.AsRedirect(String.Format("/photo/{0}#comments", photoSlug));
	}
	else
	{
		// No photo found with this slug, we'll just redirect to the homepage
		return Response.AsRedirect("/");
	}
};

On line 5 we’re asking Simple.Data for the Id of the photo the comment should be added to. If no photo is found, we just redirect to the homepage (line 31).

On line 13 we are taking advantage of the ModelBinding capabilities of Nancy. It will try to map the incoming request parameters to the properties of the provided class, Comment in this case. That’s why we gave the input fields in our view the same name as the corresponding properties in the Comment class. The three parameters (Id, PhotoId and Approved) we pass to the Bind<T>() method are blacklisted properties. This means that the Nancy ModelBinder will ignore these properties. We do this to prevent someone from manually posting to our route and proving for example an Id-parameter.

If you’re can’t get this to compile because the Bind<T>() method can’t be found, you’ll probably need to add a using Nancy.ModelBinding; since the Bind<T>() method is actually an extension method for the NancyModule class and it is located in the Nancy.ModelBinding namespace.

Next we check if the entered comment is valid or not. If it is, we ask Simple.Data to add it to the database for us. As you can see this is also very easy to do. You can just pass an object and Simple.Data will figure out what to do with it. If the comment isn’t valid, we set a session variable called commenterror to true. Finally we redirect to the page of the photo to which the comment was or should have been added.

Enabling Session

On line 23 of the Post["/{slug}/addcomment"] route, we set a session variable. If you run the application at this time, that would not work because the session state in Nancy is disabled by default. To enable it, we need to add a bootstrapper to our application. This, like most of the things in Nancy, is easier than it sounds.

Just add a class called PhotoBootstrapper to the root of our project, let it inherit from DefaultNancyBootstrapper and add an override for the InitialiseInternal method:

public class PhotoBootstrapper : DefaultNancyBootstrapper
{
	protected override void InitialiseInternal(TinyIoC.TinyIoCContainer container)
	{
		base.InitialiseInternal(container);
		Nancy.Session.CookieBasedSessions.Enable(this, "ThePassphrase", "SomeSeasoning", "HeresMyHMAC");
	}
}

That’s it. Nancy is smart enough to detect your bootstrapper and use it instead of it’s own default bootstrapper. The four parameters we need to pass to CookieBasedSessions.Enable() are an IApplicationPipelines, a passphrase, a salt and and and hmacPassphrase respectively. You should pick something more random than the values I have provided of course.

Displaying the comments

Now that users are able to add comments, we need to add some code so the comments actually get displayed. This needs to be done in two places: the Get["/{slug}"] route in the PhotoModule class and the Get["/"] route in the RootModule class.

Here is the code for the Get["/{slug}"] route:

Get["/{slug}"] = parameters =>
{
	string slug = (string)parameters.slug;
	Models.Photo photo = DB.Photos.FindBySlug(slug);

	if (photo == null)
	{
		// No photo found with this slug, we'll just redirect to the homepage
		return Response.AsRedirect("/");
	}
	else
	{
		var model = new Models.PhotoDetail();
		model.Photo = photo;

		model.PreviousSlug = DB.Photos
		                       .Query()
							   .Select(DB.Photos.Slug)
							   .Where(DB.Photos.Published == true && DB.Photos.DatePublished < photo.DatePublished.Value)
							   .OrderByDatePublishedDescending()
							   .Take(1)
							   .ToScalarOrDefault<string>();

		model.NextSlug = DB.Photos
		                   .Query()
						   .Select(DB.Photos.Slug)
						   .Where(DB.Photos.Published == true && DB.Photos.DatePublished > photo.DatePublished.Value)
						   .OrderByDatePublished()
						   .Take(1)
						   .ToScalarOrDefault<string>();

		IEnumerable<Models.Comment> comments = DB.Comments
		                                            .FindAll(DB.Comments.PhotoId == model.Photo.Id && DB.Comments.Approved == true)
													.Cast<Models.Comment>();

		if (comments != null) model.Comments = comments.ToList();

		bool commenterror = false;
		if (Boolean.TryParse(Convert.ToString(Session["commenterror"]), out commenterror))
		{
			model.ErrorMessage = "Please fill out all required fields and make sure the email address you enter is valid.";
			Session.Delete("commenterror");
		}

		return View["photodetail", model];
	}
};

We ask Simple.Data for a list of comments for the requested photo (lines 32-34) and add it to our view model (line 36). Next we check if the session variable commenterror is set to true or not. If it is, we add an error message to our view model and delete the session variable.

Since the comments also need to be displayed on the homepage, we need to add some code to the Get["/"] route as well:

Get["/"] = parameters =>
{
	var photos = DB.Photos.FindAllByPublished(true).OrderByDatePublishedDescending().Take(2);
	List<Models.Photo> photoList = photos.ToList<Models.Photo>();

	if (photoList.Count > 0)
	{
		var model = new Models.PhotoDetail();
		model.Photo = photoList[0];
		model.NextSlug = String.Empty;

		if (photoList.Count > 1) model.PreviousSlug = photoList[1].Slug;
		else model.PreviousSlug = String.Empty;

		IEnumerable<Models.Comment> comments = DB.Comments
		                                         .FindAll(DB.Comments.PhotoId == model.Photo.Id && DB.Comments.Approved == true)
												 .Cast<Models.Comment>();

		if (comments != null) model.Comments = comments.ToList();

		return View["photodetail", model];
	}
	else
	{
		return View["nophoto"];
	}
};

That’s it!

That’s it for today. Quite a lot of code, but nothing to complicated. As you have seen, Nancy and Simple.Data really make things easy for you!

As usual, the complete code is available on GitHub.

This entry was posted in Development, MyPhotoBlog and tagged , . Bookmark the permalink.

2 Responses to Building a photoblog with Nancy and Simple.Data Part 6: Adding comments

  1. Scott Rogers says:

    The line:
    Nancy.Session.CookieBasedSessions.Enable(this, “ThePassphrase”, “SomeSeasoning”, “HeresMyHMAC”);
    wouldn’t compile for me – i replaced it with:
    Nancy.Session.CookieBasedSessions.Enable(this);

    and things compiled and worked.
    Great set of blog posts!

  2. Kristof says:

    @Scott – Yeah, that something that was changed in version 0.7 of Nancy. This post was still using 0.6. In post number 7 I’m updating to version 0.7 and making the change to the CookieBasedSessions.