5,699,997 members and growing! (25,377 online)
Email Password   helpLost your password?
Web Development » ASP.NET » Samples     Intermediate License: The Code Project Open License (CPOL)

Building a Web Message Board using Visual Studio 2008, Part I - The Basic Message Board

By Rama Krishna Vavilala

This article builds a web based message board and uses several new technologies introduced with Visual Studio 2008 such as LINQ, WCF Web Programming, WCF Syndication, ASP.NET ListView, ASP.NET DataPager etc.
XML, CSS, SQL, C# (C# 1.0, C# 2.0, C# 3.0, C#), Windows (Windows, WinXP, Win2003, Vista), .NET (.NET, .NET 3.5, .NET 3.0, .NET 2.0), SQL Server (SQL 2005, SQL Server), IIS (IIS 6, IIS 7, IIS), Visual Studio (VS2008, Visual Studio), ADO.NET, WCF, ASP.NET, LINQ, Design

Posted: 22 Dec 2007
Updated: 30 Dec 2007
Views: 55,690
Bookmarked: 173 times
Announcements
Loading...



Search    
Advanced Search
Sitemap
59 votes for this Article.
Popularity: 8.12 Rating: 4.58 out of 5
3 votes, 5.1%
1
2 votes, 3.4%
2
1 vote, 1.7%
3
3 votes, 5.1%
4
50 votes, 84.7%
5

Message Board in Safari

Table of Contents

Introduction

Note: This article is not meant to be a tutorial on LINQ. For other nice introductory articles on LINQ, refer to the following articles on CP and on MSDN:

I started working on this article to get a hands-on experience with various new features of Visual Studio 2008 and of .NET Framework 3.5. I wanted to work on an example that can utilize almost all of the new features and yet be simple enough to understand. This is where the idea of a message board came to me. I thought of ways to use VSTO, WCF, Silverlight, LINQ in all its flavors, and the new ASP.NET controls. However, the project became too big to handle, and so, instead of one big article, I have decided to make it a multi-part article. In each part, I want to utilize some VS 2008/.NET 3.5 feature and extend the message board. Eventually, I want to end up with a threaded discussion forum like that of CP. This article is the first part in the series, and it builds a basic message board.

Visual Studio 2008/.NET Framework 3.5 Features Introduced in the Article

The article introduces the following new features in VS 2008:

  1. LINQ to SQL - The article shows how to map existing business objects to a relational database without using the LINQ to SQL designer. It also shows how to create a database using LINQ to SQL and how to execute raw database commands.
  2. LINQ to Objects - At various places in building the message board, LINQ to Objects has been used to simplify the handling of collections. In later parts, we will see more advanced features of LINQ to Objects.
  3. WCF Web Programming Model and Syndication - .NET 3.5 introduced the web programming model to WCF. WCF services can be easily accessed using raw HTTP GET and HTTP POST requests as opposed to constructing elaborate messages. WCF also introduces the Syndication API which allows construction and parsing of ATOM and RSS feeds.
  4. Time Zone Management Classes in .NET 3.5 - Finally, there is a class in .NET 3.5 for handling time zones. The TimeZoneInfo class can be used to factor in time zones when handling DateTime objects. The message board website makes use of this class to display the user's date and time information in the time zone of their choice.
  5. ASP.NET ListView and DataPager - The ASP.NET ListView control is a new member in the family of other data bound controls such as GridView, DataList, and Repeater. This control offers ultimate flexibility and customization capabilities when using ASP.NET data binding.
  6. New IDE and Language Features - Throughout the article, I will show the new C# language features and the VS 2008 IDE features which are cool and make life easier.

At different places, I want to indicate clearly the rationale of using or not using a feature. I will try to include information on why to use a feature rather than how to use a feature. Remember, this is only the first part; there are more to come in the later parts. Let's start by looking at what the application does.

A Quick Overview of the Message Board Web Application

The message board application allows users to post messages so that others can view it. This first version has the basic message board functionality, and we will add lots of features to it in the coming parts. The primary purpose of this article is to introduce the new features in VS 2008. As we move along, I intend to develop a "Production Quality" message board. The message board is cross-browser compatible, and has been tested with the following browsers:

  1. Internet Explorer 7.0
  2. Mozilla Firefox 2.x
  3. Opera 9.x
  4. Safari for Windows

Here are some features of the message board as implemented in this article:

  1. Users can post and view messages, both anonymously or as registered users.
  2. The site makes use of ASP.NET membership to manage users, and can plug in with any ASP.NET membership provider such as the SQL membership provider or the Active Directory membership provider.
  3. Users can view the website in three different themes: Default, Outlook, and Floating. All this is accomplished using CSS and ASP.NET themes.
  4. Users can view the time a message was posted in a time zone of their choice, which they can configure.
  5. RSS and ATOM Syndication support.

Message Board Architecture and Design

With ASP.NET, it is pretty simple to create a message board website without writing any code, and just by using designer and declarative programming. You can create a database with appropriate tables, drag and drop data source and data bound controls, and you have a website ready. Such websites serve as excellent prototypes; however, our aim here is to eventually build a "Production Quality" website to which we will add more and more features, and hence the design needs to be flexible. Apart from web based access, we will also need to provide a service based access to the website that will allow desktop and other external applications to interact with the message board. Keeping all these things in mind, I came up with a "layered" architecture for the website. The following diagram shows the different layers and the Visual Studio projects associated with each layer.

So, we have a typical three tier architecture: Presentation, Data, and the Business Logic Layer. Let's look at each layer one by one.

Core Layer

The core or the business logic layer, as it is commonly called, has API to access the message board. This code is independent of the way message board data is stored or cached. This way, the consumers of the Message Board API do not have to worry about caching or the specifics of the data store. The underlying data store can be changed, and the code accessing the Message Board API, such as the web presentation layer, does not need to be changed. Let's examine the classes in the Message Board API one by one.

Message

Since we have a message board website and the message board consists of messages, we need some way to represent a message. The Message class, as shown in the diagram below, is meant for that purpose.

The Message Class

Each message has an Id property of type int, which uniquely identifies the message in the message board. The purpose of the Subject, Text and the DatePosted properties should be clear from their names. There are two properties, PostedBy and PostedById, to identify the author of a message. The PostedBy property is of type string, and it is the name of the user who posted the message. The PostedById property needs a little explanation. One of the design goals of the message board website was to make use of the ASP.NET membership API. This saves us the trouble of writing code for user and password management. The site should be able to use any of the membership providers, either custom or built-in. The other advantage of using ASP.NET membership is that WCF can use ASP.NET membership for authentication. This will come in handy when we will expose message board services using WCF in the next article in the series. The ASP.NET membership API is built so that it works with different kinds of providers and each provider has their own unique way of identifying a user. For example, the SQL Membership Provider identifies a user with a Guid, and the Active Directory Membership Provider identifies the user using a security identifier (SID). The value which is used by a provider to identify a membership user uniquely is called Provider User Key, and is available as a property of the MembershipUser class. This value can be used in a call to Membership.GetUser to obtain a membership user. As our code should work with any membership provider, we use the PostedById field to store the provider user key as a string value.

The final property which needs explanation is the Frozen property, which is a read only property of type bool. This field is not persisted in any form, and is used to indicate whether the Message object can be modified. The Message objects should not be modified after they have been loaded from the database as these objects should be thread-safe as they can be used from multiple threads. If two threads access or modify the same message object simultaneously, the message object might get in an inconsistent state. To prevent such a thing from happening, the Frozen property is used. If the property is true, the object cannot be modified, and setting any property throws an InvalidOperationException. This property can be set by calling the Freeze method in the Message object as shown:

public Message Freeze()
{
   this.Frozen = true;
   return this;
}

public bool Frozen { get; private set; }

private void CheckImmutable()
{
   if (Frozen)
     throw new InvalidOperationException(Resources.ObjectFrozen);
}

public DateTime DatePosted
{
  get { return _datePosted; }
  set 
  { 
     CheckImmutable(); 
     _datePosted = value; 
  }
}

The CheckImmutable function checks whether the Frozen property is true, and it throws an exception if it is the case. Notice, the setter of DatePosted first calls CheckImmutable to make sure that the object is not frozen and then sets its backing field. The same is the case with the rest of the properties. The Frozen property uses the new C# feature of auto-implemented properties. As you can see, the getters and setters do not have any bodies. The compiler automatically generates a backing field for the property. The name of the backing field is cryptic, and so the field cannot be accessed from C# code. Thus, the only way to interact with the auto-implemented properties, both inside and outside of the class, is through the getter and the setter. Apart from saving a few key strokes while typing, using auto-implemented properties makes code easier to refactor.

IMessageProvider

There might be different ways to store and retrieve messages. For example, we can store the messages in a database and retrieve it from there, or for performance reasons, we can cache some messages and retrieve a message from the database only when it is not in the cache. If we use a database, we can use different APIs: LINQ, DataReader etc., to access messages from the database. In other words, there are different strategies to store and retrieve messages. The IMessageProvider interface encapsulates the strategy to retrieve and open messages.

IMessageProvider

The diagram above shows the IMessageProvider and four different implementations of it:

  1. The LinqMessageProvider uses LINQ to SQL to access messages from a SQL Server 2005 database.
  2. The NonLinqMessageProvider uses the classic technique of SqlConnection, SqlCommand, and SqlDataReader. It has been provided so that LINQ to SQL code can be compared with the classic code. We will also do some performance and load testing with the website using both the providers.
  3. The WebCacheMessageProvider works in conjunction with another message provider. It caches a set of messages in the ASP.NET cache to improve the performance. We will see the details of the WebCacheMessageProvider in a later article in this series.
  4. The ServiceMessageProvider uses the WCF service to store and retrieve messages on a server different than the web server. This message provider will be useful when there are plenty of web servers spread across geographical locations. We will see this class in a later article in the series.

Let's look at the methods in the IMessageProvider interface:

IEnumerable<Message> GetRecentMessages(int idSince, int start, int maximum);
This method retrieves a specified range of messages, whose Ids are greater than a specified message ID. This method is designed specifically for paging at source (database) for maximum efficiency.
idSince This parameter indicates that the retrieved messages should be more recent than the message ID with idSince. More recent messages have a greater Id than older messages.
start This indicates the first message to retrieve (sorted in descending order by Id) from the list of messages that match the criteria (> lastId).
maximum This indicates the maximum number of messages to retrieve.
int GetMessageCount();
Retrieves the total number of messages in the message board.
int AddMessage(string subject, string text, string postedBy, string postedById, 
               DateTime datePosted);
Adds (posts) a new message, and returns the Id of the newly posted message.
subject The subject of the message.
text The message text.
postedBy The name of the user who posted the message.
postedById The string representation of the ASP.NET membership user ID of the user who posted the message.
datePosted The date (and also the time) when the message was posted.
IEnumerable<Message> GetMessageById(int id);
Retrieves a message with a given Id. This method returns an object that implements IEnumerable. If there is no message with the given ID, the returned enumerable object has no items; however, if a message exists with the ID, the returned enumerable is a one element collection.
id The Id of the message which is to be retrieved.

The rationale behind returning IEnumerable from the GetMessageById needs a little more explanation. We could have alternatively returned a Message object. The returned Message object would have been null if the message could not be found. The advantage of using an IEnumerable is that it can directly be used in data binding, and can also be used with LINQ to Objects. Also, we don't expect end users of the API to use the IMessageProvider interface directly. An intermediary wrapper will be used to access the services of the message provider, so an overload can always be added in the wrapper. In the next section, we will examine this wrapper.

MessageSource

So, we have a Message class and a IMessageProvider interface. The question now arises, how can the presentation layer or other consumers of the Message Board API use message providers to access the messages? The first intuition is that the consumers can instantiate a class that implements IMessageProvider and then call its methods. Such a design ,even though it works, is not ideal as it defeats the purpose of isolating the consumers of the message board API from the way the messages are stored and retrieved. This is where the MessageSource class comes into picture.

The MessageSource class is a static class that has similar methods to the IMessageProvider interface. The consumers of the message board API use this class to access the messages from a provider. Here is how the MessageSource class looks like:

MessageSource

The methods in the MessageSource class are similar to the methods in the IMessageProvider interface. In fact, most of the methods simply delegate the call to an instance of a class implementing IMessageProvider. For example, here is an implementation of GetMessageCount.

public static class MessageSource
{
  private static IMessageProvider _actualMessageProvider = 
    CreateMessageProvider();

  public static int GetMessageCount()
  {
    return _actualMessageProvider.GetMessageCount();
  }
  
  ....//Rest if the code not shown
}

Notice that the MessageSource class uses a static member variable named _actualMessageSource, which is instantiated in the CreateMessageProvider function. The CreateMessageProvider reads a type name from the configuration file and instantiates the class.

private static IMessageProvider CreateMessageProvider()
{
  string typeName = 
    ConfigurationManager.AppSettings["MessageBoard-MessageProviderType"];
  Type type = Type.GetType(typeName, true);

  return (IMessageProvider)Activator.CreateInstance(type);
}

Using this mechanism ensures that different message providers can be used without recompiling the application. All that needs to be done is to change the configuration setting. Here, is how the configuration setting is specified:

<configuration>
  <appSettings>
   <add key="MessageBoard-MessageProviderType"
       value="MessageBoard.DataAccess.Linq.LinqMessageProvider,
       MessageBoard.DataAccess.Linq"/>

It may be argued that the appSetting section is not the best place for specifying this setting, instead there should be a custom configuration setting. I whole heartedly agree with the statement. At this point, I do not want to get into the task of writing a custom configuration section. We will do so later, and keep the first few parts of the article simple.

The other methods, GetRecentMessages and GetMessageById, simply delegate the call to the _actualMessageProvider. The AddMessage method is slightly different. Unlike its counterpart in the IMessageProvider interface, the AddMessage method in the MessageSource class takes only two parameters: the subject and the text. This method computes the rest of the arguments to pass to the _actualMessageProvider.

public static void AddMessage(string subject, string text)
{
   //Get the current membership user
   MembershipUser user = Membership.GetUser();
   string postedById = String.Empty;
   string postedBy;

    if (user == null)
    {
      postedBy = Resources.Anonymous;
    }
    else
    {
      postedById = user.ProviderUserKey.ToString();
      postedBy = user.UserName;
    }

     _actualMessageProvider.AddMessage(subject, text, 
                       postedBy, postedById, DateTime.Now.ToUniversalTime());
}

The method first calls the Membership.GetUser function to obtain the current MembershipUser. The GetUser function automatically obtains the MembershipUser from the current HttpContext or the thread's principal, and it returns null if the user is anonymous. If a MembershipUser is obtained, the postedById variable is set to the provider user key and the postedBy variable is set to the user name. Finally, a call is made to the _actualMessageProvider. Notice, the last parameter: DateTime.Now.ToUniversalTime(). All date and time information is stored in universal time. We could have saved the date and time using the local time zone, but saving in universal time has advantages that it is independent of any sort of daylight savings. Also, if the application is deployed in a web farm consisting of servers dispersed across time zones, the date and time information will still be saved correctly. Now, let's move on to the data access layer and see some LINQ to SQL in action.

Data Access Layer

The data access layer consists of two independent projects. One that uses LINQ to SQL, and the other project using the classic command, connection, and reader method to access the data. The other project has been provided just for comparison purposes. In a later article on, we will load-test the website using both LINQ and non-LINQ and see the difference between the two.

Both the projects use the same underlying database schema. Currently, it is the simplest possible database schema as we have only one table to save messages. The Messages table is shown below:

Messages Table

The Id column is an identity column and also a primary key. Fortunately, we are using ASP.NET Membership, and so we don't have to worry about having tables for users and profile. We will; however; extend this simple database schema in a later article when we will add the feature of message tags and user signatures.

Given this database table, it is pretty easy to implement an IMessageProvider that reads and writes Messages to the table. The general steps, if we are not using LINQ to SQL, are as follows:

  1. Create a connection object.
  2. Create a command object.
  3. Assign the SQL command text to the command object.
  4. Assign parameter values to the command object.
  5. Execute the command.
  6. If the command returns rows, read each row and populate a Message object from the row.

For example, here is how the implementation of GetRecentMessages looks like, without LINQ:

public IEnumerable<Message> GetRecentMessages(int lastId, int start,
                                                    int count)
{
  List<Message> messages = new List<Message>();

  using (SqlConnection conn = new SqlConnection(ConnectionString))
  using (SqlCommand cmd = new SqlCommand(GETRECENTMESSAGESSQL, conn))
  {
    conn.Open();

    cmd.Parameters.AddWithValue("@id", lastId);
    cmd.Parameters.AddWithValue("@start", start);
    cmd.Parameters.AddWithValue("@count", count);

    using (SqlDataReader reader = cmd.ExecuteReader())
    {
        while (reader.Read())
        {
            int id = reader.GetInt32(0);
            string subject = reader.GetString(1);
            string text = reader.GetString(2);
            string postedBy = reader.GetString(3);
            string postedById = reader.GetString(4);
            DateTime postedDate = reader.GetDateTime(5);

            Message m = new Message(id, subject, text, postedBy, postedById,
                postedDate);
            messages.Add(m);
        }
    }
  }

  return messages;
}

The GETRECENTMESSAGESSQL looks like the following:

const string GETRECENTMESSAGESSQL = @"WITH OrderedMessages AS
(
  SELECT id, subject, text, postedBy, postedById, DatePosted,  
  ROW_NUMBER() OVER (ORDER BY DatePosted Desc) AS 'RowNumber'
  FROM Messages WHERE Id <= @id
) 
SELECT * FROM OrderedMessages
WHERE RowNumber BETWEEN @start and @start + @count - 1";

The above SQL uses the ROW_NUMBER() function introduced with SQL Server 2005 to page the results. We could have alternatively used a stored procedure and put the SQL inside the stored procedure. In that case, the GETRECENTMESSAGESSQL would have looked like the following:

const string GETRECENTMESSAGESSQL = 
      "EXEC GetRecentMessages @Id, @start, @count";

The above SQL looks a little simpler, but has no impact on the implementation of the GETRECENTMESSAGES method. The GETRECENTMESSAGE would look exactly the same, whether using stored procedures or raw SQLs. Also, for simple statements like these, stored procedures are not necessarily efficient. Another issue to be noted is that there is a contract between the C# code and the SQL code for the order in which columns should appear in the results. If the column order in SQL is changed, the code breaks. We can circumvent this issue by finding the ordinal of each column by name in the reader and then using that ordinal to obtain the value, but it will make the code a little more messy. This is where LINQ to SQL comes in handy. Let's see the equivalent LINQ to SQL code.

public IEnumerable<Message> GetRecentMessages(int lastId, int start,
                                              int count)
{
    using (MessageBoardDataContext context = CreateDataContext())
    {
        var messages = from m in context.Messages
                       where m.Id > lastId
                       orderby m.DatePosted descending
                       select m;
        
        var messagesInRange = messages.Skip(start).Take(count);
        
        return messagesInRange.ToList();
    }
 }

First, we create an object of type MessageBoardDataContext, which is a class derived from System.data.Linq.DataContext. The DataContext class serves as the source of all the objects (entities) accessed via LINQ to SQL over a particular database connection. We will see how to create a DataContext class in a while. Next, the we use LINQ to write a query to get the messages greater than a particular Id and ordered in a descending order. Out of these messages, we select a range of messages by calling Skip and Take. Finally, we return the list of messages by calling ToList. The beauty of LINQ to SQL is that the query gets sent to the database only when ToList is called. LINQ to SQL automatically generates a query to issue to the database. The following is the automatically generated query in response to a call to GetRecentMessages(0, 20, 25).

exec sp_executesql N'SELECT [t1].[Id], [t1].[Subject], [t1].[Text], 
    [t1].[PostedBy], [t1].[PostedById], [t1].[DatePosted]
FROM ( 
SELECT ROW_NUMBER() OVER (ORDER BY [t0].[DatePosted] DESC) AS [ROW_NUMBER],
    [t0].[Id], [t0].[Subject], [t0].[Text], [t0].[PostedBy], 
    [t0].[PostedById], [t0].[DatePosted]
    FROM [Messages] AS [t0]
    WHERE [t0].[Id] > @p0
    ) AS [t1]
WHERE [t1].[ROW_NUMBER] BETWEEN @p1 + 1 AND @p1 + @p2
ORDER BY [t1].[ROW_NUMBER]',N'@p0 int,@p1 int,@p2 int',@p0=0,@p1=20,@p2=25

The SQL code is ugly and complex, but the good news is that it was all generated automatically. Another point to note is that the generated code will be different if SQL 2000 was being used, as SQL Server 2000 does not support the ROW_NUMBER() function.

Let's review some of the advantages of LINQ to SQL over the classic method, and then we will jump into the details of how LinqMessageProvider has been implemented. Here are some of the things I liked about the LINQ implementation:

  1. We did not hard code any SQL in the code. The SQL was automatically generated, which is always a nice thing.
  2. Unlike the classic counterpart where we had to write code such as string postedBy = reader.GetString(3);, we did not have to worry about the ordinal position of values in the result set, as we did not generate the result set in the first place.

In this particular case, there is no doubt that LINQ to SQL has resulted in much more cleaner code, but it did not come for free. Let's see what background work we had to do to get the LINQ to SQL code working, in the next section.

ORM Mapping using LINQ to SQL

You may have heard that LINQ to SQL is an ORM tool. It allows you to map objects to a relational database schema. In our case, we want to map the properties of the Message class to the Messages table. In most of the LINQ to SQL tutorials you will find on the web, you will see either:

  1. The LINQ to SQL designer used to generate classes from a database.
  2. Special attributes applied to classes in the Business Object Layer to map them to the database.

However, in the MessageBoard.DataAccess.Linq project, we don't use either of the above techniques. LINQ to SQL provides a way to use an external XML file map. Here is the XML file for mapping the Message class to the Messages table:

<?xml version="1.0" encoding="utf-8"?>
<Database Name="MessageBoard" 
           xmlns="http://schemas.microsoft.com/linqtosql/mapping/2007">
  <Table Name="Messages" Member="Messages">
    <Type Name="MessageBoard.Message">
      <Column Name="Id" Member="Id" 
         DbType="Int NOT NULL IDENTITY"IsPrimaryKey="true" 
              IsDbGenerated="true"
              AutoSync="OnInsert" />
      <Column Name="Subject" Member="Subject" 
          DbType="NVarChar(128) NOT NULL"
          CanBeNull="false" />
      <Column Name="Text" Member="Text" 
             DbType="NVarChar(MAX) NOT NULL" 
             CanBeNull="false" UpdateCheck="Never" />
      <Column Name="PostedBy" Member="PostedBy" 
            DbType="NVarChar(256) NOT NULL" 
            CanBeNull="false" />
      <Column Name="PostedById" Member="PostedById" 
            DbType="NVarChar(256) NOT NULL" 
            CanBeNull="false" />
      <Column Name="DatePosted" Member="DatePosted" 
            DbType="SmallDateTime NOT NULL" />
    </Type>
  </Table>
</Database>

At the root, we have a Database element to which we give an identifying name: MessageBoard. The Database element consists of a single element: Table, in our case. The Name attribute of the Table element indicates the name of the table, and the Member attribute indicates that the data context has a member named Messages that corresponds to this table. Within the Table element, there is a Type element that indicates the type to which the table maps to. The Name attribute of the Type element indicates the class name of the Message class. The type element has several child elements named Column, which indicate the name of the column in the database table and the name of the property to which the column maps to. The Member attribute indicates the property name, and the Name attribute indicates the column name.

How was the XML file generated?

Well, I did not hand code the entire XML file. What I did was to use the SqlMetal tool to generate the XML mapping file and then modify it. First, I ran the following command:

sqlmetal /server:.\SQLExpress /database:MessageBoard /map:MessageBoard.xml 
         /code:discard.cs

This indicates that a mapping file named MessageBoard.xml should be generated for the database named MessageBoard in SQLExpress Server. Also, notice the argument /code:discard.cs. The SqlMetal tool wants to generate the C# classes regardless of whether you want them or not. In our case, I did not need the classes, so we just delete the resultant C# file.

Next, I modified the XML file generated by SqlMetal to change the type names to match the actual type names in the project. It's kind of strange that there is no support for generating LINQ to XML files automatically in Visual Studio 2008.

At this point, we have an XML file that maps the properties of the Message object to the columns in the Message table. This XML mapping is saved as an embedded resource in the MessageBoard.DataAccess.Linq project. Next, we need to create a class derived from DataContext and load the XML mapping into it. This class also has a member named Messages of type Table<Message>. Here is the code for the MessageBoardDataContext class:

/// Data context for the message board
public class MessageBoardDataContext : DataContext
{
    /// Create a data context that uses the connection string
    /// specified in the configuration file
    public MessageBoardDataContext()
        : this(_connectionString)
    {
    }

    /// Create a data context for a specific connection string
    public MessageBoardDataContext(string connectionString)
        : base(connectionString, _mappingSource)
    {
    }

    // Default connection string read from the config file
    static string _connectionString
        = ConfigurationManager.ConnectionStrings[
            "LocalSqlServer"].ConnectionString;

    //Initialize the mapping source read from the 
    //XML file in the resource
    static XmlMappingSource _mappingSource
        = GetMappingSource();

    private static XmlMappingSource GetMappingSource()
    {
        return XmlMappingSource.FromStream(
            typeof(MessageBoardDataContext)
             .Assembly
             .GetManifestResourceStream(
             "MessageBoard.DataAccess.Linq.Mapping.xml"));
    }

    /// Member that maps to the Messages table in the database
    public Table<Message> Messages
    {
        get
        {
            return GetTable<Message>();
        }
    }
}

As we already discussed, LINQ to SQL has two different ways to map properties and fields in classes to columns in tables. The first one is via attributes specified on the properties and the classes, and the second is through an XML file. LINQ to SQL has a general purpose abstract base class called MappingSource to handle mapping. Currently, two concrete implementations of this class are AttributeMappingSource and XmlMappingSource. The DataContext class has a constructor that takes a MappingSource. In the above code snippet, we create an XmlMappingSource from the XML file stored in the assemblies resource. This is done in the GetMappingSource method by calling XmlMappingSource.FromStream and passing it the manifest resource stream.

That's all! The MessageBoardDataContext uses the XML mapping we supplied to map the Messages table to the Message class, and we are able to use LINQ to SQL. The advantage of using an XML file for mapping is that it does not clutter the actual code with LINQ to SQL specific attributes. The other advantage is that the business layer classes can be designed independently of LINQ to SQL.

One final thing I want to cover before we move on to the presentation layer of the Message Board, is adding new messages to the database. Here is the implementation of the AddMessage method:

public int AddMessage(string subject, string text, string postedBy,
       string postedById, DateTime datePosted)
{
   using (MessageBoardDataContext context = CreateDataContext())
   {
        context.ObjectTrackingEnabled = true;

        Message message = new Message();
        message.Subject = subject;
        message.Text = text;
        message.PostedBy = postedBy;
        message.PostedById = postedById;
        message.DatePosted = datePosted;
        context.Messages.InsertOnSubmit(message);

        context.SubmitChanges();

        //After calling submit changes the Id is automatically updated
        return message.Id;
   }
}

After creating the DataContext object, the ObjectTrackingEnabled property is set to true. What this means is that the data context keeps track of objects to figure out if they have been updated or needs to be inserted. (We need to do this because CreateDataContext sets the property to false.) Next, we create a Message object and assign all its properties, except the Id property. Then, we call InsertOnSubmit which indicates to the data context that a particular Message object has to be inserted in the database when the SubmitChanges method is called. The SubmitChanges makes a batch call to the database, sending all the updates (if any) and inserts. At the end of SubmitChanges, the Message object is inserted in the database. Not only that, the Id property of the Message object is automatically populated from the database table's identity value. This is because of the following line in the XML file:

<Column Name="Id" Member="Id" DbType="
              Int NOT NULL IDENTITY" IsPrimaryKey="true" 
              IsDbGenerated="true" 
              AutoSync="OnInsert" />

The AutoSync="OnInsert" and IsDbGenerated="true" attributes indicate that a specific property is an identity property and needs to be automatically loaded after insert. Doing so causes the following insert statement to be generated by LINQ to SQL:

INSERT INTO [Messages]([Subject], [Text], [PostedBy], [PostedById], 
    [DatePosted])
VALUES (@p0, @p1, @p2, @p3, @p4)

SELECT CONVERT(Int,SCOPE_IDENTITY()) AS [value]

After the insert, the SELECT CONVERT(Int,SCOPE_IDENTITY()) statement obtains the identity value inserted in the table.

Why CONVERT(Int,SCOPE_IDENTITY())?

The SCOPE_IDENTITY() function returns a decimal, and since the Id property is of type int , LINQ to SQL generates the SQL query which uses the CONVERT function.

We will revisit LINQ to SQL one more time when we will see how to create a new database using LINQ to SQL. Now, let's move on to the presentation layer using ASP.NET.

The Presentation Layer

The presentation layer consists of an ASP.NET website and a C# assembly. The website consists of ASP.NET pages, style sheets, and images. As far as possible, the website is coded declaratively. Any non-trivial code required to support the website is placed in the MessageBoard.Web project. The aim is to have unit tests all non trivial code, so that they can be tested properly for quality. The unit testing and the load testing will come as part of a separate article. It also helps in separating concerns: a designer can work on the web pages independently of the developer and vice versa. Personal preference has a lot to do too with partitioning projects in that way, and so it is by no means the way to partition projects. As we move along in the next few articles, we will add ASP.NET server controls to the MessageBoard.Web project.

Let's start with the Web.Config file. For maximum performance, it is better to turn off the view state and the session state in all the pages. Don't get me wrong, view state and session state have their place in developing websites, but in the message board site, they will not be needed. So, we add the following configuration entry:

<configuration>
  <system.web>
    <pages enableViewState="false" 
    enableSessionState="false" >

Let's look at the website map:

Web Site Drawing

The site has a master page named Site.master, which contains things like the header and the navigation bar. All pages in the website use the same master page. The main page is Default.aspx, which shows a list of all the messages with subject, user, date posted, and partial text. When the user clicks on any of the messages, he is taken to the Message.aspx page which shows the full details of the message. The site has a Login.aspx page which a user can use to login to the site, and a Register.aspx page which he can use to register. The Login.aspx page and the Register.aspx page make use of the ASP.NET Login and the CreateUserWizard controls. The Settings.aspx page is where the user can change the settings such as his time zone. Feed.svc is a web service that provides RSS and ATOM feeds. The site has CSS files corresponding to the two themes: Outlook and Floating. The site uses CSS for layout and positioning, so except for the standard ASP.NET controls which use tables for layout, you will not find any tables on the site. Later on, we can use the ASP.NET CSS friendly control adapters to remove the remaining tables.

The Master Page

Look at the following screenshots of the Default.aspx and the Message.aspx pages.

Default.aspx:

The Default.aspx page

Message.aspx:

Message.aspx

You will notice that the top banner and the navigation panel with the theme selector on the left are the same. These common elements have been put in the site's master page, site.master, so they appear on every page. While putting links and banner in the master page is quite common and nothing complex, the tricky part is to put the theme selector on the master page. The problem is that a web page theme can only be changed in the Page's PreInit event and the master pages do not get applied until after it. In fact, the master page is applied after the theme is set.

In order for the theme selector to work properly with the master page, we have to rely on the Global.asax file to change the theme, as shown below:

void Application_PreRequestHandlerExecute(object sender, EventArgs e)
{
    Page page = Context.Handler as Page;

    if (page == null)
        return;

    //Get the theme
    ...
   
    page.PreInit += delegate
    {
        page.Theme = theme; 
        
        //Update the cookie ...
    };
}

The PreRequestHandlerExecute execute function is called just before ASP.NET starts the page life cycle. The page object is instantiated, but its life cycle has not started yet. At this point, we handle the PreInit event of the page and set the theme appropriately.

How we obtain the theme is a little complex because of the way master pages work. Normally, when you have no master pages, you can safely assume that controls which are on the form have same ID in the client as on the server. However, with master pages, it's no longer true. ASP.NET generates a client ID based on where the page appears in the control hierarchy. Take a look at the following control:

<asp:DropDownList runat="server" 
    class="ThemeSelector" 
    ID="ThemeSelector" AutoPostBack="true" />

We cannot assume that the client side ID of the above control will be ThemeSelector. This is because it is on a master page. So, the client ID might be something like ctl000_ThemeSelector. To access the value during post back is a two step process.

First, we need a way to obtain the client ID of the control. This is done by adding a hidden field to the form which contains the unique ID of the control. The unique ID of the control is the name by which its post-back value can be extracted from the Request.Forms collection. So, the following code in the master page ensures that the hidden field contains the unique ID of the ThemeSelector drop down list.

protected override void OnPreRender(EventArgs e)
{
    base.OnPreRender(e);

    //Fill the theme selector drop down
    ....
    
    //Register an hiiden field that gives the ID of the selector control
    //as we don't know what it will be
    this.Page.ClientScript.RegisterHiddenField("ThemeSelectorId"
                  , ThemeSelector.UniqueID);
}

The value of the selected theme in the drop down list can be obtained from the following code in the Global.asax PreRequestHandlerExecute event handler:

string themeSelectorId = Request["ThemeSelectorId"];
string theme = Request[themeSelectorId];

The first line looks up the ID of the control, and the next line obtains the post-back value (which will be the selected value in the drop down list). Finally, the theme is applied correctly in the Pre_Init event. That's the only code in the site.master page worth mentioning. Now, let's move on to the main page (Default.aspx).

The Main Page

The main page is where we get a chance to use the exciting new control in ASP.NET 3.5: the ListView control. The ListView control is another data bound control in the family of GridView, Repeater, and the DataList controls. The nice thing about it is that it combines the good features of all these controls. The following table compares list view with other controls:

ListView GridView Repeater DataList
Paging Support Yes Yes No No
Flexible Layout Yes No (only tabular layout possible) Yes No (layout uses tables)
Editing Support Yes Yes No Yes
Insertion Support Yes No No No

The ListView control thus has the good features of the GridView, Repeater, and the DataList controls. The best thing about the ListView is that it offers a lot of control on the generated HTML. Therefore, it is possible to generate a clean HTML well suitable for CSS layouts. This, however, does not mean that ListView is best for all data binding scenarios. For displaying tabular data, I still think that GridView is the best. However, I do find it hard to come up with scenarios where the Repeater and DataList are better than the ListView. If you can think of one, please be free to post it in as a comment. Now that I have built a lot of expectations about the ListView, let's see if it meets the expectations.

Using the ListView Control to Display and Insert Data

The list view is a data bound control; it can bind to any data source supported by ASP.NET. For the message board, we have to use the ObjectDataSource control as we have data available through the MessageSource type. Remember that the message board API consumers are unaware of the way data is stored. The ASP.NET ObjectDataSource control comes in pretty handy to expose the message board data, accessed via the MessageSource class, to data bound controls, in a declarative fashion.

<asp:ObjectDataSource ID="messageDataSource" 
       runat="server" 
       TypeName="MessageBoard.MessageSource"
       
       SelectMethod="GetRecentMessages"
       StartRowIndexParameterName="start" 
       MaximumRowsParameterName="count" 
       
       SelectCountMethod="GetMessageCount"
       
       EnablePaging="True" 
       
       InsertMethod="AddMessage">

The ObjectDataSource can get and save data from a business object by calling methods on the business objects. We indicate the type name of the business object using the TypeName property of the ObjectDataSource control. In our case, it will be the MessageSource class, which is our only interface to the message board. We indicate the method GetRecentMessages as the one which will be responsible for providing data. The signature of the GetRecentMessages looks like the following:

IEnumerable<Message> GetRecentMessages(int start, int count)

The start parameter indicates the index of the first message to obtain from the list of all messages, and the count parameter indicates the maximum number of messages to obtain. Thus, the StartRowIndexParameterName has been set to start, and the MaximumRowsParameterName has been set to count. The ObjectDataSource control automatically uses these properties to automatically page data at the source. Also notice the SelectCountMethod which is set to GetMessageCount. The ObjectDataSource calls this method to estimate the maximum number of messages available for paging purposes. Finally, we set the InsertMethod property to AddMessage. This method will be responsible for adding messages to the message board.

The ListView control can be bound to the data source using the following markup:

      <asp:ListView ID="messageListView" runat="server" 
      DataSourceID="messageDataSource" ..>

Now that the list view is bound to the data source, the list view can generate individual items from the IEnumerable<Message> object returned by the GetRecentMessages method. ListView is a very flexible control; it allows you to control all aspects of the layout, including the root HTML element which will contain the items. Let's see how we specify the markup to generate the list view control.

When designing a web page, I normally start with a raw HTML page that will resemble the output of the ASP.NET web page, and then generate the markup for the ASP.NET page. The HTML code which I came up with looks like the following:

<div class="header">
    <span class="subject">Subject</span> 
    <span class="postedBy">Posted By</span> 
    <span class="datePosted">Date Posted</span>
</div>
<div id="messageList">

  <div class="message" >
    <h2 class="subject"><a> ... </a></h2>
    <div class="postedBy">
       <b>Posted By: </b>...</div>
    <div class="datePosted">
      <b>Date Posted:&nbsp</b> ...</div>
    <div class="text"> ... </div>
  </div>

  <div class="message" >...
  </div>

</div>

So, basically, we have a div with an ID of messageList and within which we have all the message items. To get such an output using the ListView control, we have to take the following steps.

First, we have to specify the LayoutTemplate of the ListView control, as follows:

<asp:ListView ...>
   <LayoutTemplate>
      <div class="header">
        <span class="subject">Subject</span> 
        <span class="postedBy">Posted By</span> 
        <span class="datePosted">Date Posted</span>
    </div>
    <div id="messageList">
        <asp:PlaceHolder runat="server" 
        ID="itemPlaceHolder" />
    </div>
  </LayoutTemplate>
  ...
</asp:ListView>

Of particular interest is the PlaceHolder control whose ID is itemPlaceHolder. The ListView control replaces the place holder with the rendered HTML for each individual item in the data source. Now, we need to specify how a particular item in the data source should be rendered in HTML. This is done by specifying the ItemTemplate of the ListView, as shown below:

<asp:ListView ...>
<ItemTemplate>
    <div class="message">
        <h2 class="subject">
            <a href='<%# MessageUrl %>'>
                <%# Message.Subject %>
            </a>
        </h2>
        <div class="postedBy">
            <b>Posted By: </b><%# Message.PostedBy %
        ></div>
        <div class="datePosted">
            <b>Date Posted:&nbsp</b>
                <%# MessageDateInUsersTimeZone %>
        </div>
        <div class="text">
            <asp:Literal runat="server" 
                Text='<%# MessagePreviewText %>' 
                Mode="Encode" />
        </div>
    </div>
</ItemTemplate>
...
</asp:ListView>

Notice the ASP.NET data binding expressions. If you are accustomed to using data binding expressions, you will observe the lack of an Eval function. To make the code cleaner and also to avoid Reflection when using data binding, I have properties declared as follows, in the Page class:

private MessageBoard.Message Message
{
    get { return Page.GetDataItem() as MessageBoard.Message;  }
}

private string MessageUrl
{
    get { return "Message.aspx?id=" + Message.Id.ToString(
        CultureInfo.InvariantCulture); }
}

private string MessageDateInUsersTimeZone
{
    get { return Utility.GetFormattedTime(Message.DatePosted); }
}

private string MessagePreviewText
{
    get { return Utility.GetPreviewText(Message.Text)); }
}

The Message property needs a little explanation. The Page.GetDataItem method returns the current item that is being data bound. Thus, from within the ItemTemplate, the GetDataItem will return a Message object. The Message will return the current Message object that is being data bound. When this property is accessed outside of a data binding context, an exception will be thrown.

Paging ListView using the DataPager Control

Unlike GridView, the ListView control does not have any way to specify the template for the pager controls. Instead, the ListView control implements an interface named IPageableItemContainer. Any control that implements this interface can be paged using the new DataPager control. So, in order to get the paging to work, we need to drop a DataPager control and set its properties:

<asp:DataPager ID="topPager" runat="server" 
   PagedControlID="messageListView" 
   QueryStringField="start" 
   PageSize="25">

We first set the PagedControlID property and assign it the ID of the ListView control. We also set the PageSize property that indicates the maximum number of items in the page. In future versions, we will load the PageSize from user settings. The final thing to note here is the property named QueryStringField whose value is set to start. The real beauty of the DataPager control is that it can automatically use the value of this query string field (start) to move the control to a specific page. This saves us from writing any imperative code.

Finally, you can customize the pager controls in a variety of ways. You can indicate how you want it to appear: numeric, next/previous buttons, or custom, or a combination of all. The following code shows how to get a pager with both next/previous buttons and the numbers.

<asp:DataPager ID="topPager" runat="server">
    <Fields>
        <asp:NextPreviousPagerField ButtonType="Button" 
            ShowFirstPageButton="True" 
            ShowNextPageButton="False"
            ShowPreviousPageButton="True" 
            FirstPageText="<<" 
            LastPageText=">>" NextPageText=">"
            PreviousPageText="<" 
            RenderDisabledButtonsAsLabels="false" />
        <asp:NumericPagerField />
        <asp:NextPreviousPagerField ButtonType="Button" 
           ShowLastPageButton="True" 
            ShowNextPageButton="True"
            ShowPreviousPageButton="False" 
            RenderDisabledButtonsAsLabels="false" 
            NextPageText=">"
            LastPageText=">>" />
    </Fields>
</asp:DataPager>

The above code generates a pager that looks like the following:

Pager

We have seen how to display page data in the list view, now let's move on to inserting data: posting a new message.

Inserting Data using the ListView Control

The greatest advantage of the ListView control is that not only can it display data, but it also has support for inserting and editing data. In the case of the message board, we will not be editing data but we will sure be inserting data as we allow users to post messages. Instead of developing a separate page or using a different control like the FormView control, we can directly use the ListView control to insert data. Recall that in the declaration of the ObjectdataSource control, we set the property InsertMethod to "AddMessage" . This indicates that the ObjectDataSource control should call AddMessage when it is requested to insert new data. Who exactly requests the ObjectDataSource to insert new data? That will be any data bound control bound to the ObjectDataSource with support for inserting data. In our case, it is the ListView.

To enable a ListView to insert data, we need to do two things. First, we need to set the InsertItemPosition property to either "LastItem" or "FirstItem". This controls where exactly the ListView will display a panel with editable controls which a user can use to insert data. Next, we need to define the InsertItemTemplate and put editable data bound controls in it:

<asp:ListView InsertItemPosition="LastItem" ... >
...
<InsertItemTemplate>
    <div id="newMessagePanel">
        <a id="newMessageBookmark"></a>
        <h2>
            Post a Message</h2>
        <div id="subjectPanel">
            <asp:Label CssClass="subjectLabel" 
            runat="server" 
                     AccessKey="S" 
                     Text="Subject:"