ASP.NET MVC Application Building: Forums #3 – Post Messages

In this series of blog entries, I build an entire ASP.NET MVC forums application from start to finish. In this blog entry, I add the functionality to the Forums application that enables users to post new messages and replies.

Before reading this blog entry, you should read the previous two blog entries in this series:

ASP.NET MVC Application Building: Forums #1 – Create the Perfect Application – In this first entry, I explain the overall goals of the ASP.NET MVC Forums application. I emphasize the importance of Software Design Principles and justify my choice to use test-driven development.

ASP.NET MVC Application Building: Forums #2 – Create the First Unit Test – In the second entry, I build the first unit test and create the Index() action which returns a collection of messages.

Rethinking the Message Class

I quickly discovered that my original design of the Message class just won’t work. The Message class is supposed to represent each message posted to the forum. The same Message class is used to represent the original message in a thread and each of the replies to the thread.

Originally, the Message class included the following properties:

· Id

· ParentId

· Author

· Subject

· Body

· EntryDate

The ParentId property represents the message to which the current message is a reply. For example, if you are replying to message 1 then the ParentId is 1. If the message starts a new thread then the Parentid is NULL.

Unfortunately, I discovered that I also needed to add a ParentThreadId property. Otherwise, the database logic for retrieving all of the messages in a particular thread was just too ugly. Therefore, I modified the Message class to support the following properties:

· Id

· ParentThreadId

· ParentMessageId

· Author

· Subject

· Body

· EntryDate

Notice that the Message class now has both a ParentThreadId property and a ParentMessageId property. I use the ParentThreadId property to retrieve all of the messages in the same thread. The ParentMessageId can be used within a view to display the relationship between different messages in the same thread.

Creating the New Unit Tests

Because I am building the Forums application by following good test-driven development practices, I started by creating a new set of unit tests. The new unit tests are contained in Listing 1.

Listing 1 – Controllers\ForumControllerTest.cs

using System.Collections.Generic;
using System.Web.Mvc;
using LinqToSqlExtensions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.Web.Mvc;
using MvcFakes;
using MvcForums.Controllers;
using MvcForums.Models;

namespace MvcForums.Tests.Controllers
{

    [TestClass]
    public class ForumControllerTest
    {
        private IForumRepository _repository;
      
        [TestInitialize]
        public void Initialize()
        {
            // Create fake data
            var dataContext = new FakeDataContext();
            dataContext.Insert(new Message(1, null, null, "Robert", "Welcome to the MVC forums!", "body1"));
            dataContext.Insert(new Message(2, 1, 1, "Stephen", "RE:Welcome to the MVC forums!", "body2"));
            dataContext.Insert(new Message(3, 1, 2, "Robert", "RE:Welcome to the MVC forums!", "body3")); 
            dataContext.Insert(new Message(4, null, null, "Mark", "Another message", "body4"));
            dataContext.Insert(new Message(5, 4, 4, "Stephen", "Yet another message", "body5"));
            dataContext.Insert(new Message(6, 4, 5, "Jane", "Yet another message", "body6")); 
            
            // Return repository
            _repository = new ForumRepository(dataContext);
        }

        [TestMethod]
        public void IndexReturnsMessageThreads()
        {
            // Arrange
            var controller = new ForumController(_repository);
            
            // Act
            var result = controller.Index() as ViewResult;
            
            // Assert
            var model = result.ViewData.Model as List<Message>;
            Assert.AreEqual(2, model.Count);
        }

        /// <summary>
        /// Verifies Thread(1) action returns 3 messages and all messages
        /// have either an Id or ParentThreadId of 1
        /// </summary>
        [TestMethod]
        public void ThreadReturnsMessagesInThread()
        {
            // Arrange
            var controller = new ForumController(_repository);

            // Act
            var result = controller.Thread(1) as ViewResult;

            // Assert
            var model = result.ViewData.Model as List<Message>;
            Assert.AreEqual(3, model.Count); // 3 messages in first thread
            foreach (var m in model)
            {
                Assert.IsTrue(m.Id == 1 || m.ParentThreadId == 1);
            }
        }

        [TestMethod]
        public void NewThreadIncrementsThreadCount()
        {
            // Arrange
            var controller = new ForumController(_repository);
            var originalThreadCount = _repository.SelectThreads().Count;

            // Act
            var form = new FormCollection();
            form.Add("author", "Stephen");
            form.Add("subject", "New Thread!");
            form.Add("body", "Body of new thread");
            var result = controller.Create(form);

            // Assert
            var newThreadCount = _repository.SelectThreads().Count;
            Assert.AreEqual(originalThreadCount + 1, newThreadCount);  
        }

        [TestMethod]
        public void NewReplyIncrementsMessageCount()
        {
            // Arrange
            var controller = new ForumController(_repository);
            var originalMessageCount = _repository.SelectMessages(1).Count;

            // Act
            var form = new FormCollection();
            form.Add("parentThreadId", "1");            
            form.Add("parentMessageId", "1");
            form.Add("author", "Stephen");
            form.Add("subject", "New Thread!");
            form.Add("body", "Body of new thread");
            var result = controller.Create(form);

            // Assert
            var newMessageCount = _repository.SelectMessages(1).Count;
            Assert.AreEqual(originalMessageCount + 1, newMessageCount);
        }

        [TestMethod]
        public void NewReplyDoesNotIncrementsThreadCount()
        {
            // Arrange
            var controller = new ForumController(_repository);
            var originalThreadCount = _repository.SelectThreads().Count;

            // Act
            var form = new FormCollection();
            form.Add("parentThreadId", "1");
            form.Add("parentMessageId", "1");
            form.Add("author", "Stephen");
            form.Add("subject", "New Thread!");
            form.Add("body", "Body of new thread");
            var result = controller.Create(form);

            // Assert
            var newThreadCount = _repository.SelectThreads().Count;
            Assert.AreEqual(originalThreadCount, newThreadCount);
        }

    }
}

The test class in Listing 1 contains the following unit tests:

· IndexReturnsMessageThreads() – Verifies that the Index() action returns the 2 messages with a NULL ParentThreadId.

· ThreadReturnsMessagesInThread() – Verifies that the Thread() action returns all of the messages in a given thread.

· NewThreadIncrementsThreadCount() – Verifies that posting a new message without a ParentThreadId increases the number of the message threads.

· NewReplyIncrementsMessageCount() – Verifies that posting a reply to a thread increases the number of messages in the thread.

· NewReplyDoesNotIncrementThreadCount() – Verifies that posting a reply does not increase the total number of threads.

These unit tests should be pretty straightforward to understand. They all take advantage of the fake DataContext class behind the scenes. That way, the database itself is not actually exercised or modified.

Notice that the unit tests themselves do not interact with the DataContext class directly. Instead, the unit tests only interact with the ForumRepository class. That way, if we decide to change our data access technology – for example, we swap LINQ to SQL for NHibernate – then we do not need to change our unit tests. If we change data access technologies then we only need to change the Initialize() method.

Modifying the Forum Repository

In order to satisfy the new unit tests, I had to add two new methods to the ForumRepository class. Fortunately, because we are using LINQ to SQL, implementing the new methods required very little code. The new version of the ForumRepository class is contained in Listing 2.

Listing 2 – Models\ForumRepository.cs

using MvcFakes;
using System.Linq;
using System.Data.Linq;
using LinqToSqlExtensions;
using Microsoft.Web.Mvc;
using System.Collections.Generic;

namespace MvcForums.Models
{
    public class ForumRepository : IForumRepository
    {
        private IDataContext _dataContext;

        public ForumRepository()
            : this(new DataContextWrapper("conForumsDB", "~/Models/ForumsDB.xml"))
        { }
        
        public ForumRepository(IDataContext dataContext)
        {
            _dataContext = dataContext;
        }

        public IList<Message> SelectThreads()
        {
            var messages = _dataContext.GetTable<Message>();
            var threads = from m in messages
                          where m.ParentThreadId == null
                          select m;
            return threads.ToList();
        }

        public IList<Message> SelectMessages(int threadId)
        {
            var messages = _dataContext.GetTable<Message>();
            var threads = from m in messages
                          where (m.Id == threadId || m.ParentThreadId == threadId)
                          select m;
            return threads.ToList();
        }


        public Message AddMessage(Message messageToAdd)
        {
            _dataContext.Insert(messageToAdd);
            return messageToAdd;
        }
    }
}

The SelectMessages() method returns a collection of messages for a particular thread. The AddMessage() method adds a new message to the database. This method is called when adding a message that starts a new thread or when adding a reply to an existing thread.

Modifying the Forum Controller

The Forum controller, like the ForumRepository, also got two new methods. The modified version of the Forum controller is contained in Listing 3.

Listing 3 – Controllers\ForumController.cs

using System;
using System.Web.Mvc;
using MvcForums.Models;
using Microsoft.Web.Mvc;

namespace MvcForums.Controllers
{
    public class ForumController : Controller
    {
        private IForumRepository _repository;

        public ForumController()
            : this(new ForumRepository())
        { }

        public ForumController(IForumRepository repository)
        {
            _repository = repository;
        }

        public ActionResult Index()
        {
            ViewData.Model = _repository.SelectThreads();
            return View("Index");
        }

        public ActionResult Create(FormCollection form)
        {
            // Create new message
            var messageToCreate = new Message();
            if (!String.IsNullOrEmpty(form["parentThreadId"]))
                messageToCreate.ParentThreadId = int.Parse(form["parentThreadId"]);
            if (!String.IsNullOrEmpty(form["parentMessageId"]))
                messageToCreate.ParentMessageId =  int.Parse(form["parentMessageId"]);
            messageToCreate.Author = form["author"];
            messageToCreate.Subject = form["subject"];
            messageToCreate.Body = form["body"];
            messageToCreate.EntryDate = DateTime.Now;

            // Add to database
            _repository.AddMessage(messageToCreate);

            // Redirect
            return RedirectToAction("Index");
        }

        public ActionResult Thread(int threadId)
        {
            ViewData.Model = _repository.SelectMessages(threadId);
            return View("Thread");
        }

    }
}

The Create() action creates a new message in the database. This action takes the form parameters passed to the action and generates an instance of the Message class. Next, the Message class is added to the database with the help of the ForumRepository.AddMessage() method.

The Thread() action returns a collection of messages that corresponds to a particular thread. This action delegates all of its work to the ForumRepository.SelectMessages() method.

Success!

After making the changes to the ForumRepository and ForumController classes, our new unit tests pass. We now have a grand total of 5 unit tests for our Forums application (see Figure 1).

Figure 1 – Happy Test Results

clip_image002

Creating the Views

Strictly speaking, there is no real reason to create views for our Forums application yet. We don’t actually need to run the application to make sure that the application works. Executing our unit tests provides much better evidence that the application works correctly than running the application and trusting that we would notice anything wrong with our eyeballs.

However, I am human. I like to run an application occasionally just as a sanity check. Therefore, I created two simple views that corresponds to the Index() action and the Thread() action.

The view for the Index() action is contained in Listing 4. This view simply displays all of the threads in a bulleted list (see Figure 2).

Figure 2 – The Forums Index view

clip_image004

Listing 4 – Views\Forum\Index.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="MvcForums.Views.Forum.Index" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title></title>
</head>
<body>
    <div>
    
    <ul>
    <% foreach (var m in ViewData.Model)
       { %>

        <li>
           <%= Html.Encode(m.Author) %>
           <%= Html.ActionLink(m.Subject, "Thread", new {threadId=m.Id})%>       
        </li>

    <% } %>
    </ul>

    </div>
</body>
</html>

The Index view renders a link to each thread. If you click on a link, you arrive at the Thread view. The Thread view is contained in Listing 5. The Thread view displays all of the messages in a particular thread (see Figure 3).

Listing 5 – Views\Forum\Thread.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Thread.aspx.cs" Inherits="MvcForums.Views.Forum.Thread" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title></title>
</head>
<body>
    <div>

    <ul>
    <% foreach (var m in ViewData.Model)
       { %>

        <li>
           <%= Html.Encode(m.Author) %>
           <%= Html.Encode(m.Subject) %>
           <p>
            <%= Html.Encode(m.Body) %>
           </p>       
        </li>

    <% } %>
    </ul>
    
    
    </div>
</body>
</html>

Figure 3 – The Thread view

clip_image006

Both the Index and Thread views are strongly typed views. If you look at the code-behind classes for either view then you’ll see that that the views are strongly typed to cast the ViewData.Model property to an instance of the List<Movie> collection. For example, the code-behind class for the Index view is contained in Listing 6.

Listing 6 – Views\Forums\index.aspx.cs

using System.Collections.Generic;
using System.Web.Mvc;
using MvcForums.Models;

namespace MvcForums.Views.Forum
{
    public partial class Index : ViewPage<List<Message>>
    {
    }
}

A Moment of Reflection

When you read tutorials and books on test-driven development, they always advocate that you never stray from the following pattern of steps:

(1) Write a test

(2) Satisfy the test

(3) Refactor

In my experience, the process isn’t quite as pure. While working on the forums application, I always started by writing a test. However, sometimes when I started to build the code to satisfy the test I discovered that I made some crazy assumptions and I needed to change the test.

I allowed the test to dictate the code that I needed to write. I used the test as an expression of the requirements that I needed to satisfy. However, in the process of writing the code to satisfy the test, I sometimes discovered that the requirements represented by the test were confused. I needed to rethink the design of the application. In these situations, I ended up reworking the test.

I don’t think that there is anything wrong with this process. The design of an application must evolve when you discover flaws in the design while writing the actual code. This experience shows that test-driven development is not a purely linear process.

Summary

In this blog entry, I modified the Forums application so that it supports posting new messages and posting new replies. We created four new unit tests to drive our development.

In the next blog entry, we need to tackle the issue of validation. We need to validate the form data posted to a controller action before we insert the form data into the database. Also, we need to display validation error messages in our views.

Download the Code

8 Comments

Comments have been disabled for this content.