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:
- 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.
- 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.
- 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.
- 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.
- 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.
- 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:
- Internet Explorer 7.0
- Mozilla Firefox 2.x
- Opera 9.x
- Safari for Windows
Here are some features of the message board as implemented in this article:
- Users can post and view messages, both anonymously or as registered users.
- 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.
- Users can view the website in three different themes: Default, Outlook, and Floating. All this is accomplished using CSS and ASP.NET themes.
- Users can view the time a message was posted in a time zone of their choice, which they can configure.
- 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.
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.
The diagram above shows the IMessageProvider
and four different implementations of it:
- The
LinqMessageProvider
uses LINQ to SQL to access messages from a SQL Server 2005 database.
- 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.
- 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.
- 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 Id s 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:
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();
}
....
}
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)
{
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:
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 Message
s to the table. The general steps, if we are not using LINQ to SQL, are as follows:
- Create a connection object.
- Create a command object.
- Assign the SQL command text to the command object.
- Assign parameter values to the command object.
- Execute the command.
- 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:
- We did not hard code any SQL in the code. The SQL was automatically generated, which is always a nice thing.
- 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:
- The LINQ to SQL designer used to generate classes from a database.
- 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:
="1.0" ="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:
public class MessageBoardDataContext : DataContext
{
public MessageBoardDataContext()
: this(_connectionString)
{
}
public MessageBoardDataContext(string connectionString)
: base(connectionString, _mappingSource)
{
}
static string _connectionString
= ConfigurationManager.ConnectionStrings[
"LocalSqlServer"].ConnectionString;
static XmlMappingSource _mappingSource
= GetMappingSource();
private static XmlMappingSource GetMappingSource()
{
return XmlMappingSource.FromStream(
typeof(MessageBoardDataContext)
.Assembly
.GetManifestResourceStream(
"MessageBoard.DataAccess.Linq.Mapping.xml"));
}
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();
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:
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:
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;
...
page.PreInit += delegate
{
page.Theme = theme;
};
}
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);
....
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: </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: </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:
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:" /><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):
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
.
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.
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:
<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
.
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);
context.Response.AppendCookie(cookie);
}
}
This method is invoked from the settings page when the user saves the settings:
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:
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);
}
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.
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:
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:
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:
- The
OperationContract
attribute ensures that the particular interface method can be invoked via WCF.
- The
WebGet
attribute ensures that the method can be accessed via a plain HTTP GET request.
- 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.
public class FeedService : IFeedService
Here are the steps to use the Syndication API to return a feed:
- Create a
SyndicationFeed
object.
- Populate the
Uri
, Description
, Title
and other properties of SyndicationFeed
.
- Create a collection of
SyndicationFeedItem
s 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
.
public SyndicationFeedFormatter GetLatestMessages(FeedFormat format)
{
Uri rootUri = GetRootUri();
SyndicationFeed feed = new SyndicationFeed(
Resources.MessageBoard
, Resources.MessageBoardDescription,
rootUri
);
feed.Items = from m in MessageSource.GetRecentMessages(0, 10)
select CreateSyndicationItem(m, rootUri);
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 SyndicationFeedItem
s. 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
.
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,
m.Id.ToString(),
new DateTimeOffset(m.DatePosted, new TimeSpan(0)));
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:
<%@ 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:
<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:
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:
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:
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:
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:
- Open the solution file in Visual Studio 2008.
- Build the project
- 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.
<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.
- Right click on the file Install.ashx which is in the message board website project, and click on View in browser.
- This will launch the browser and automatically create the database.
If you have SQL Server, follow these steps:
- Open the solution.
- Build the solution.
- Open the Install.sql file in the MessageBoard website project. This file is in the Install folder.
- 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.
- Now, you need to change the web.config file to use the new connection string.
<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:
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:
MessageBoardDataContext dataContext = new MessageBoardDataContext(connectionString);
if (!dataContext.DatabaseExists())
{
dataContext.CreateDatabase();
response.Write("Adding ASP.NET Services.... ");
response.Flush();
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.
- Part II - Posting Messages using Microsoft Word.
- Part III - AJAXifying the Message board.
- Part IV - Adding Tags and Threaded Discussions.
- 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.