Click here to Skip to main content
15,861,125 members
Articles / Web Development / CSS
Article

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

Rate me:
Please Sign up or sign in to vote.
4.90/5 (83 votes)
30 Dec 2007CPOL47 min read 372.5K   3.7K   333   61
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.

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.

Image 2

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:

C#
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:

C#
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.
idSinceThis 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.
startThis indicates the first message to retrieve (sorted in descending order by Id) from the list of messages that match the criteria (> lastId).
maximumThis indicates the maximum number of messages to retrieve.
C#
int GetMessageCount();
Retrieves the total number of messages in the message board.
C#
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.
subjectThe subject of the message.
textThe message text.
postedByThe name of the user who posted the message.
postedByIdThe string representation of the ASP.NET membership user ID of the user who posted the message.
datePostedThe date (and also the time) when the message was posted.
C#
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.
idThe 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.

C#
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.

C#
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:

XML
<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.

C#
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:

C#
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:

SQL
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:

C#
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.

C#
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).

SQL
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
<?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:

C#
/// 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:

C#
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:

XML
<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:

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:

XML
<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:

C#
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:

XML
<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.

C#
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:

C#
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:

ListViewGridViewRepeaterDataList
Paging SupportYesYesNoNo
Flexible LayoutYesNo (only tabular layout possible)YesNo (layout uses tables)
Editing SupportYesYesNoYes
Insertion SupportYesNoNoNo

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.

XML
<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:

C#
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:

XML
<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:

XML
<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:

XML
<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:

XML
<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:

C#
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:

XML
<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.

XML
<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:

XML
<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:" /><br />
            <asp:TextBox ID="Subject" 
            CssClass="subjectTextBox" runat="server" 
                     Text='<%# Bind("Subject") %>' 
                     Columns="60" Rows="1" />
        </div>
        <div id="textPanel">
            <asp:Label CssClass="textLabel" 
                     runat="server" AccessKey="T" 
                     Text="Text:" /><br />
            <asp:TextBox ID="Text" 
                    CssClass="textTextBox" runat="server" 
                    Text='<%# Bind("Text") %>'
                TextMode="MultiLine" Rows="10" 
                    Columns="60" />
        </div>
        <div id="buttonPanel">
            <asp:Button ID="PostMessage" 
                    CommandName="Insert" 
                    runat="server" Text="
                    Post Message" />
            <asp:Button ID="Cancel" runat="server" 
                    CommandName="Cancel" Text="Cancel" 
                    CausesValidation="False" />
        </div>
    </div>
</InsertItemTemplate>

</asp:ListView>

The figure below shows how the ASP.NET markup shown above renders (without any styles):

Insert Item Template

The important point to observe here is how the subject and text fields are bound. The Text property of the Subject is set to Bind("Subject") and that of the text field is set to Bind("Text"). Where are we getting the strings that we pass to the Bind method? The answer lies in the signature of MessageSource.AddMessage.

C#
public static void AddMessage(string subject, string text)

The parameter passed to Bind (this is not a method or a function but just a special word understood by the ASP.NET data binding engine) corresponds to the parameter names of AddMessage. The other important thing to note here is the CommandName property of the Post Message button. This indicates, when the Post Message button is pressed, the ListView should data bind and insert the data. If you don't specify the command name as insert, the ListView will not be able to insert data.

The message detail page (Message.aspx) also uses a ListView control. I will skip the details here as it is very similar to the main page. I will also skip through the Logon and Registration pages which use the standard ASP.NET Login controls. We will move to the settings page and see how the site manages time zones.

Managing Time zones in the Message Board

If your website is on the internet and catered to a global audience, you have to address the issue of time zones when displaying date and time. For example, in the message board site, the users are displayed messages along with the date and time when the message was posted. The issue here is what date and time should be shown to the user. Here are a few options:

  • Display the data and times in the time zone of the server machine. This will not make much sense to the users who are not in the same time zone as the web server’s time zone. Plus, the users need to know the server time zone.
  • Display all times in GMT or UTC. This option has the disadvantage that it requires the users to translate the times from GMT/UTC to their own time, which is not a very user friendly option.
  • Display the time span instead of displaying the actual time. It shows how many days, hours, and minutes ago a particular forum post was made. This works fine, but sometimes it is not very intuitive.
  • The option employed in the message board is to show the date and time in the time zone of the user accessing the website. This option makes the most sense to the user; however, it requires some bit of programming. Fortunately, working with time zones is a lot easy with the new TimeZoneInfo class introduced in .NET Framework 3.5.

In the message board website, the users are given the option to specify a time zone in which they want to view the date and time of posted messages. This is done in the Settings page where the user is provided with a drop down to select a time zone.

Settings Time Zone

The Time Zone drop down list displays all the available time zones. This list can be obtained by using the services of the TimeZoneInfo class. The TimeZoneInfo class provides a static method named GetSystemTimeZones which returns an array of TimeZoneInfo objects. Using the ObjectDataSource control, a combo box can be bound to the values returned by the TimeZoneInfo class, as shown below:

XML
<asp:DropDownList runat="server" ID="TimeZoneList" 
    CssClass="TimeZoneList" 
    DataSourceID="TimeZoneSource"
    DataTextField="DisplayName" 
    DataValueField="Id"  
    AppendDataBoundItems="true">
    <asp:ListItem Text="Universal Time" 
        Value="UTC" />;
</asp:DropDownList>;
<asp:ObjectDataSource runat="server" 
   TypeName="System.TimeZoneInfo" 
   SelectMethod="GetSystemTimeZones"
    ID="TimeZoneSource" />

We bind the Text to the DisplayName and the Value to the time zone Id. A time zone can be uniquely identified using a string ID. The TimeZoneInfo class provides a method called FindSystemTimeZoneById for this purpose. Notice that we do have to add the UTC time zone separately as it is not returned in the list of time zones. This is the default time zone for any user who has not configured the time zone. Once the user saves the changes to his settings, the selected time zone ID is saved to a cookie. This is done in a method called SaveTimeZone in a class called TimeZoneUtility.

C#
class TimeZoneUtility 
{
...
 public static void SaveTimeZoneInfoInCookie(TimeZoneInfo info)
 {
    HttpContext context = HttpContext.Current;

    if (context == null)
      throw new InvalidOperationException(Resources.NullHttpContext);
            
    HttpCookie cookie = new HttpCookie(CookieName, info.Id);
    cookie.Expires = DateTime.Now.AddYears(1); //Expire after a year
    context.Response.AppendCookie(cookie);
  }
}

This method is invoked from the settings page when the user saves the settings:

C#
protected void SaveChanges_Click(object sender, EventArgs e){
     TimeZoneUtility.SaveTimeZoneInfoInCookie(
           TimeZoneUtility.GetTimeZoneFromId(TimeZoneList.SelectedValue));
     Response.Redirect("~/Default.aspx");
}

The time zone info can be retrieved from the cookie using the following code:

C#
public static TimeZoneInfo GetTimeZoneInfoFromCookie()
{
    HttpContext context = HttpContext.Current;

    if (context == null)
        throw new InvalidOperationException(Resources.NullHttpContext);

    HttpCookie cookie = context.Request.Cookies[CookieName];

    TimeZoneInfo info = TimeZoneInfo.Utc;

    if (cookie == null || String.IsNullOrEmpty(cookie.Value))
        return info;

    try
    {
        info = TimeZoneInfo.FindSystemTimeZoneById(cookie.Value);

    }
    catch (TimeZoneNotFoundException ex)
    {
        Trace.WriteLine(ex);
        //It's ok just return Utc               
    }

    return info;
}

The above function extracts a time zone from the cookie if present; otherwise, it returns TimeZoneInfo.Utc, which is the default. To display date and time information in the user's time zone, there is a separate function named GetFormattedTime which returns a formatted date time value in the user's time zone.

C#
public static String GetFormattedTime(DateTime dateTime)
{
   return TimeZoneUtility.ConvertToCurrentTimeZone(dateTime)
               .ToString("MMMM dd, MM:hh tt");
}

Finally, the TimeZoneUtility.ConvertToCurrentTimeZone function extracts the time zone from a cookie and converts a specified date time to the user's time zone:

C#
public static DateTime ConvertToCurrentTimeZone(DateTime dateTime)
{
    return TimeZoneInfo.ConvertTimeFromUtc(dateTime, 
                   TimeZoneUtility.GetTimeZoneInfoFromCookie());
}

Thus, the TimeZoneInfo class comes in pretty handy when working with time zones. It is a late but welcome addition to the .NET Framework. Now, let's move on to another new feature of .NET Framework 3.5: WCF Syndication API and WCF Web Programming Model.

Displaying RSS and ATOM Feeds to the User

Providing RSS or ATOM feeds is becoming a necessary feature of any website. Of course, it makes a lot of sense for the message board site to do so. When it comes to providing feeds, I could have used LINQ to XML and hand crafted something. However, WCF in .NET 3.5 provides an API to generate and parse RSS and ATOM feeds. This is a part of the assembly System.ServiceModel.Web, and the classes are in the System.ServiceModel.Syndication namespace. Why is this a part of WCF? I have no idea, but it is a welcome addition nonetheless. The advantage of using the WCF Syndication API over hand crafting something is that you don't have to go into the details of the specs of each of the feed formats. Further, you can use the same code to generate both RSS and ATOM feeds.

In the MessageBoard website, we use the syndication API in conjunction with the WCF web programming model. Let's pause briefly to discuss about the WCF web programming model. Typically, when you invoke WCF service calls, you have to construct SOAP messages and send them to the service, and the service responds back with another SOAP message. With the web programming model, you can issue a simple HTTP GET or HTTP POST request to invoke WCF service calls. This is a lot simpler than constructing SOAP messages. Let's see how the web programming model works for the Message Board.

First, we need to create a service contract:

C#
public enum FeedFormat
{
    Atom,
    Rss
}

[ServiceContract]
public interface IFeedService
{
  [OperationContract]
  [WebGet]
  [ServiceKnownType(typeof(Rss20FeedFormatter))]
  [ServiceKnownType(typeof(Atom10FeedFormatter))]
  SyndicationFeedFormatter GetLatestMessages(FeedFormat format);
}

The GetRecentMessages takes an enum parameter of type FeedFormat, which can be Atom or Rss. Given the format of the feed, it returns the feed of that format. Let's go through each of the attributes on the method one by one:

  1. The OperationContract attribute ensures that the particular interface method can be invoked via WCF.
  2. The WebGet attribute ensures that the method can be accessed via a plain HTTP GET request.
  3. The two ServiceKnownType attributes ensure that the return value SyndicationFeedFormatter can be an instance of either Rss20FeedFormatter or Atom10FeedFormatter.

If a WCF method outputs an RSS or an ATOM feed, the return value of the method should be SyndicationFeedFormatter (or one of its sub classes). WCF will serialize the SyndicationFeedFormatter object output to raw RSS or ATOM feed.

Next, we need to implement the interface as a class.

C#
public class FeedService : IFeedService

Here are the steps to use the Syndication API to return a feed:

  1. Create a SyndicationFeed object.
  2. Populate the Uri, Description, Title and other properties of SyndicationFeed.
  3. Create a collection of SyndicationFeedItems which will represent each individual message in the feed, and assign the collection to the Items property of the SyndicationFeed.

Let's see these steps as implemented in the FeedService.

C#
public SyndicationFeedFormatter GetLatestMessages(FeedFormat format)
{
    Uri rootUri = GetRootUri();
    SyndicationFeed feed = new SyndicationFeed(
             Resources.MessageBoard //Title of the feed
             , Resources.MessageBoardDescription, //Description of the feed
             rootUri //The rootUri of the web site providing the feed
          );
    //Use recent 10 message in the feed
    feed.Items = from m in MessageSource.GetRecentMessages(0, 10)
                 select CreateSyndicationItem(m, rootUri);
    
    //Return the appropriate FeedFormat
    if(format == FeedFormat.Atom)
        return new Atom10FeedFormatter(feed);
    
    return new Rss20FeedFormatter(feed);
}

First, we call a method named GetRootUri. This method gives the root URI of the web application. For example, if the application is deployed on localhost in a virtual directory named MessageBoard, the value returned by GetRootUri will be: http://localhost/MessageBoard. Once we get the root URI, we create a syndication feed with a title, description, and the root URI. The title and description are loaded from the resource file. Finally, we use LINQ to Objects, to convert a collection of the ten recent messages to a collection of SyndicationFeedItems. The method finally returns an appropriate type of SyndicationFeedFormatter depending on the value of the format parameter. This is done using a helper function called CreateSyndicationItem.

C#
private SyndicationItem CreateSyndicationItem(Message m, Uri rootUri)
{
    UriBuilder uriBuilder = new UriBuilder(rootUri);
    uriBuilder.Path += "Message.aspx";
    uriBuilder.Query = "id=" + m.Id.ToString(
        CultureInfo.InvariantCulture);

    var item = new SyndicationItem(m.Subject,
                     m.Text, 
                    uriBuilder.Uri, //URL at which the message is available
                     m.Id.ToString(), //The unique message id
                     //Time message was posted in terms of offset from UTC
                     new DateTimeOffset(m.DatePosted, new TimeSpan(0))); 
  
    //Add the authors
    item.Authors.Add(new SyndicationPerson(m.PostedBy));

    return item;
}

In the above function, we construct the URI using UriBuilder. This is the URL or the permalink at which a particular message will be available. Then, we create a SyndicationItem with the information from the Message object. The SyndicationItem takes an object of type DateTimeOffset which represents date and times in offsets from UTC.

Interestingly, the DateTimeOffset class is in mscorlib.dll. This is a new class introduced in .NET 2.0 SP1, which means that it is automatically available in .NET 3.5. This is in contrast to the TimeZoneInfo class which is present in System.Core.dll. This further complicates the entire .NET 2.0 - .NET 3.5 saga.

Now, we have a service contract and an object which implements the service contract. The service is exposed via the Feed.svc file in the Message Board website. The contents of Feed.svc are shown below:

XML
<%@ ServiceHost Language="C#"  
     Debug="true" 
     Service="MessageBoard.Web.FeedService" %>

This makes sure that our service is available through the Feed.svc file in the website. We are not done yet, we need to apply the WCF configuration in the web.config file to expose the service using the web programming model. This is done in the configuration file, as shown below:

XML
<system.serviceModel>
 <behaviors>
  <endpointBehaviors>
   <behavior name="feedHttp">
    <webHttp />
   </behavior>
  </endpointBehaviors>
  <serviceBehaviors>
   <behavior name="FeedServiceBehavior">
    <serviceDebug includeExceptionDetailInFaults="true" />
   </behavior>
  </serviceBehaviors>
 </behaviors>
 <services>
  <service behaviorConfiguration="FeedServiceBehavior"
         name="MessageBoard.Web.FeedService">
   <endpoint address=""
        behaviorConfiguration="feedHttp"
        binding="webHttpBinding"
        contract="MessageBoard.Web.IFeedService" />
  </service>
 </services>
</system.serviceModel>

The important things to note here are that the service uses webHttpBinding and the endpoint behavior includes webHttp. Both of these are necessary for the service to be accessed via the web programming model. Finally, you can access the feeds by typing in the appropriate URLs, as shown in the screenshot below:

Feed

The WCF web programming model is pretty nice, and we will revisit it in the later parts in the series. With this, we have a complete message board application; now, we will see a little detail about the site layout and themes.

Themes and Layouts

The message board site relies heavily on CSS for layout and formatting. Here is how the main page looks without any CSS applied:

Site Without CSS

The site supports two different themes: Outlook and Floating. The only difference between the themes is the CSS file behind the web pages. The HTML content of the site always remains the same. Here is how the site looks in the Outlook theme:

SiteWithOutlookTheme.JPG

The theme tries to simulate the Outlook 2007 Silver theme as much as possible. The background gradients are all made possible by using background images. As you will remember, the site does not use any HTML tables at all; the tabular layout which you see above is made possible by using a combination of relative positioning, absolute positioning, padding, and margins. The subject column automatically resizes with the window whereas the Posted By and Date Posted columns stay constant.

Finally, here is how the site looks in the Floating theme:

Floating Theme

There is a custom background image on each of the messages, and the messages have the float CSS attribute set to left. The site also uses image replacement techniques to replace the heading Message Board with a custom image. This is done by adding a background image and hiding and indenting the contents so that they don't appear. Another interesting thing about the floating theme is that only the messages scroll, the control bar on the left and the banner on the top stay fixed. This is done via fixed CSS positioning.

CSS is amazingly powerful, and it has improved a lot with Internet Explorer 7.0. The advantage of using CSS for layouts is that it helps keep the HTML clean. The clean HTML is extremely useful when using AJAX in the website. We will see in part III of the series how we can add AJAX support to the message board website.

Installation Instructions

The site needs either SQL Express or a full fledged SQL Server 2005.

If you have SQL Server Express, follow these steps:

  1. Open the solution file in Visual Studio 2008.
  2. Build the project
  3. If you have a custom instance of SQL Express which is not named SQLExpress, you need to modify the connection string settings in the web.config file.
  4. XML
    <configuration>
      <connectionStrings
      <add name="LocalSqlServer" 
        connectionString="data source=.\SQLEXPRESS;...." 
         providerName="System.Data.SqlClient"/>

    Modify the data source to use the name of the custom instance. Leave the rest of the connection string as is. Note: I have not included the full connection string here.

  5. Right click on the file Install.ashx which is in the message board website project, and click on View in browser.
  6. Install

  7. This will launch the browser and automatically create the database.

If you have SQL Server, follow these steps:

  1. Open the solution.
  2. Build the solution.
  3. Open the Install.sql file in the MessageBoard website project. This file is in the Install folder.
  4. Right click and click Execute. Select the database connection and click OK. This will install the messages table, ASP.NET SQL Services and sample data.
  5. Now, you need to change the web.config file to use the new connection string.
  6. XML
    <configuration> 
     <connectionStrings 
        <add name="LocalSqlServer" 
            connectionString=Modify the connection string
            providerName="System.Data.SqlClient"/>

About the Install Scripts

To generate the database install script, I used a feature of Visual Studio 2008, which I accidentally came across. When you right click on a data source in the Server Explorer, an option named Publish to Provider appears:

Publish to Provider

Selecting this option launches a wizard that generates script for the entire database, both for schema and the data. That is how the install.sql file was generated.

The Install.ashx file uses LINQ to SQL for creating a database. Here is the code snippet that does it:

C#
MessageBoardDataContext dataContext = new MessageBoardDataContext(connectionString);

if (!dataContext.DatabaseExists())
{
    dataContext.CreateDatabase();

    response.Write("Adding ASP.NET Services.... ");
    response.Flush();

    //Now add ASP.NET features
    SqlServices.Install(dataContext.Connection.Database, 
         SqlFeatures.All, connectionString);

    response.Write("Installing sample data....");
    response.Flush();
    
    string sampleDataSqlFile = 
          context.Request
                .MapPath("~/Install/InstallSampleData.sql");
    dataContext.ExecuteCommand(
              File.ReadAllText(sampleDataSqlFile));

    response.Write("Database created successfully!");
}
else
{
    response.Write("Database already exists");
}

The DataContext class provides a method called DatabaseExists, which, given a connection string, can figure out whether the database exists. We first use this method to check if the database exists, and then if it does not, we call the CreateDatabase method. The CreateDatabase method automatically uses the information specified in the ORM mapping and creates the database and the tables. After creating the database, we call the SqlServices.Install method to install the ASP.NET membership specific schema on the database.

Next in the Series

I have planned for the next few parts in this series as follows. I will provide the links once the articles are published.

  1. Part II - Posting Messages using Microsoft Word.
  2. Part III - AJAXifying the Message board.
  3. Part IV - Adding Tags and Threaded Discussions.
  4. Part V - Load Testing, Caching, and Performance Analysis of the Message Board.

Acknowledgements

  • My wife Radhika for writing the Non-Linq version of the IMessageProvider.
  • VuNic for quickly developing a background image for the message board site.

History

  • December 21, 2007 - First posted.
  • December 31, 2007 - Updated Series Navigation.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionA question about project references [modified] Pin
Aerryc18-Apr-11 10:17
Aerryc18-Apr-11 10:17 
GeneralBinding ObjectDataSourceControl to a collection in ASP.Net 3.5 Pin
elizas28-Apr-10 2:53
elizas28-Apr-10 2:53 
GeneralRe: Binding ObjectDataSourceControl to a collection in ASP.Net 3.5 Pin
vimalrns6-Sep-12 0:00
professionalvimalrns6-Sep-12 0:00 
GeneralMy vote of 2 Pin
Meeking11-Apr-10 5:39
Meeking11-Apr-10 5:39 
GeneralDatabase Pin
hutty7-Dec-09 15:58
hutty7-Dec-09 15:58 
GeneralGetting Errors Pin
hutty27-Nov-09 15:56
hutty27-Nov-09 15:56 
GeneralGreat Artcle, best of cp Pin
vishalMGiri1-Jul-09 4:09
vishalMGiri1-Jul-09 4:09 
GeneralSecurity Pin
jpevans10-Mar-09 4:22
jpevans10-Mar-09 4:22 
GeneralGreat article Pin
samcourbon26-Dec-08 22:35
samcourbon26-Dec-08 22:35 
GeneralRe: Great article Pin
bennyj435-Jan-09 10:44
bennyj435-Jan-09 10:44 
GeneralVery nice article Pin
Rajiv Kapadia25-Sep-08 8:13
Rajiv Kapadia25-Sep-08 8:13 
GeneralGreat Article Pin
Ashish Dhar14-Sep-08 6:15
Ashish Dhar14-Sep-08 6:15 
QuestionI need help!!! Pin
Jaywalker12324-Jul-08 18:50
Jaywalker12324-Jul-08 18:50 
QuestionGreat job, when are you going to write your next doc about WCF? Pin
malcolmxu27-Mar-08 4:55
malcolmxu27-Mar-08 4:55 
GeneralAwesome...Excellent... Pin
J. Wijaya14-Mar-08 0:18
J. Wijaya14-Mar-08 0:18 
Generalw8ing for Ajaxifying Pin
yassir hannoun20-Feb-08 13:11
yassir hannoun20-Feb-08 13:11 
Generalawsome Pin
yassir hannoun20-Feb-08 13:05
yassir hannoun20-Feb-08 13:05 
GeneralGreat Article! But.... Pin
Frank Nesse19-Feb-08 4:32
Frank Nesse19-Feb-08 4:32 
GeneralRe: Great Article! But.... Pin
Frank Nesse19-Feb-08 4:50
Frank Nesse19-Feb-08 4:50 
GeneralRe: Great Article! But.... Pin
Frank Nesse19-Feb-08 9:57
Frank Nesse19-Feb-08 9:57 
GeneralRe: Great Article! But.... Pin
Rama Krishna Vavilala19-Feb-08 10:04
Rama Krishna Vavilala19-Feb-08 10:04 
GeneralRe: Great Article! But.... Pin
Frank Nesse19-Feb-08 21:03
Frank Nesse19-Feb-08 21:03 
GeneralThanks for the superb article Pin
Nagarajan Mohan19-Feb-08 0:14
Nagarajan Mohan19-Feb-08 0:14 
GeneralRe: Thanks for the superb article Pin
Rama Krishna Vavilala19-Feb-08 0:16
Rama Krishna Vavilala19-Feb-08 0:16 
GeneralError on install Pin
QPR18-Feb-08 22:00
QPR18-Feb-08 22:00 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.