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:
- LINQ - .NET Language Integrated Query
- LINQ Blog Posts by Scott Guthrie
- LINQ articles on the Code Project
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 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 - A various places in building the message board LINQ to Object have been used to simplify 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 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 handlingDateTime
objects. The Message board web site makes use if this class to display users date and time information in time zone of their choice. - ASP.NET
ListView
andDataPager
- The ASP.NETListView
control is a new member in the family of other data bound controls such asGridView
,DataList
andRepeater
. 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 web site 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 web site, 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 web site ready. Such web sites serve as excellent prototypes; however, our aim here is to eventually build a "Production Quality" web site 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 web site 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 web site. The following diagram shows the different layers and the visual studio projects associated with each layer.
So we have a typical 3 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 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 web site and 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 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 site was to make use of the ASP.NET membership API. This saves us 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 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 Boolean
type. 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 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 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 all 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 classic technique ofSqlConnection, SqlCommand
andSqlDataReader
. 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 web site 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 theWebCacheMessageProvider
in a later article in this series. - The
ServiceMessageProvider
uses WCF service to store and retrieve messages on a server different than the web server. This message provider will be useful when there are plenty of web servers spread across geographical locations. We will see this class in a later article in the series.
Let's look at the methods in the IMessageProvider
interface:
IEnumerable<Message> GetRecentMessages(int idSince, int start, int maximum); | |
This method retrieves a specified range of messages, whose Ids are greater than a specified message Id. This method is designed specifically for paging at source (database) for maximum efficiency. | |
idSince | This parameter indicates that the retrieved messages should be more recent than the message Id with idSince . More recent messages have a greater Id than older messages. |
start | This indicates the first message to retrieve (sorted in descending order by Id) from the list of messages that match the criteria (> lastId ) . |
maximum | This indicates the maximum number of messages to retrieve. |
int GetMessageCount(); | |
Retrieves the total number of messages in the message board. | |
int AddMessage(string subject, string text, string postedBy, string postedById, DateTime datePosted); | |
Adds(posts) a new message and returns the id of the newly posted message. | |
subject | The subject of the message. |
text | The message text. |
postedBy | The name of the user who posted the message |
postedById | The string representation of the ASP.NET membership user Id of the user who posted the message. |
datePosted | The data (an 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 also can 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 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 IMessageProvider
interface. In fact most of the methods simply delegate the call to an instance of a class implementing IMessageProvider
. For example, here is an implementation of GetMessageCount
.
public static class MessageSource { private static IMessageProvider _actualMessageProvider = CreateMessageProvider(); public static int GetMessageCount() { return _actualMessageProvider.GetMessageCount(); } ....//Rest if the code not shown }
Notice that the MessageSource
class uses a static member variable named _actualMessageSource
which is instantiated in the CreateMessageProvider
function. The CreateMessageProvider
reads a type name from the configuration file and instantiates the class.
private static IMessageProvider CreateMessageProvider() { string typeName = ConfigurationManager.AppSettings["MessageBoard-MessageProviderType"]; Type type = Type.GetType(typeName, true); return (IMessageProvider)Activator.CreateInstance(type); }
Using this mechanism ensures that different message providers can be used without recompiling the application. All that needs to be done is to change the configuration setting. Here, is how the configuration setting is specified:
<configuration> <appSettings> <add key="MessageBoard-MessageProviderType" value="MessageBoard.DataAccess.Linq.LinqMessageProvider, MessageBoard.DataAccess.Linq"/>
It may be argued that the appSetting
section is not the best place for specifying this setting, instead there should be a custom configuration setting. I whole heartedly agree with the statement. At this point, I do not want to get into the task of writing a custom configuration section. We will do so later and keep the first few parts of the article simple.
The other methods - GetRecentMessages,
and GetMessageById
simply delegate the call to the _actualMessageProvider
. The AddMessage
method is slightly different. Unlike, it's counterpart in the IMessageProvider
interface, the AddMessage
method in the MessageSource
class takes only two parameters: the subject and the text. This method computes the rest of the arguments to pass to the _actualMessageProvider
.
public static void AddMessage(string subject, string text) { //Get the current membership user MembershipUser user = Membership.GetUser(); string postedById = String.Empty; string postedBy; if (user == null) { postedBy = Resources.Anonymous; } else { postedById = user.ProviderUserKey.ToString(); postedBy = user.UserName; } _actualMessageProvider.AddMessage(subject, text, postedBy, postedById, DateTime.Now.ToUniversalTime()); }
The method first calls the Membership.GetUser
function to obtain the current MembershipUser
. The GetUser
function automatically obtains the MembershipUser
from the current HttpContext
or the thread's principal and it returns null if the user is anonymous. If a MembershipUser
is obtained, the postedById
variable is set to the provider user key and the postedBy
variable is set to the user name. Finally, a call is made to the _actualMessageProvider.
Notice, the last parameter: DateTime.Now.ToUniversalTime()
. All date and time information is stored in universal time. We could have saved the date and time in local time zone, but saving is 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 zone, 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 other project uses 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 web site 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 the 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 following:
- Create a connection object
- Create a command object
- Assign the SQL command text to the command object.
- Assign any 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 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 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 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 was used to generate classes from a database.
- Special attributes where applied to classes in the Business Object Layer to map them to the database.
However, in MessageBoard.DataAccess.Linq
project case, we don't use either of the above techniques. LINQ to SQL provides a way to use external XML file map. Here is the XML file for mapping Message
class to the Messages
table.
<?xml version="1.0" encoding="utf-8"?> <Database Name="MessageBoard" xmlns="http://schemas.microsoft.com/linqtosql/mapping/2007"> <Table Name="Messages" Member="Messages"> <Type Name="MessageBoard.Message"> <Column Name="Id" Member="Id" DbType="Int NOT NULL IDENTITY"IsPrimaryKey="true" IsDbGenerated="true" AutoSync="OnInsert" /> <Column Name="Subject" Member="Subject" DbType="NVarChar(128) NOT NULL" CanBeNull="false" /> <Column Name="Text" Member="Text" DbType="NVarChar(MAX) NOT NULL" CanBeNull="false" UpdateCheck="Never" /> <Column Name="PostedBy" Member="PostedBy" DbType="NVarChar(256) NOT NULL" CanBeNull="false" /> <Column Name="PostedById" Member="PostedById" DbType="NVarChar(256) NOT NULL" CanBeNull="false" /> <Column Name="DatePosted" Member="DatePosted" DbType="SmallDateTime NOT NULL" /> </Type> </Table> </Database>
At the root we have a Database
element to which we give an identifying name: MessageBoard
. The Database
element consists of a single element: Table
in our case. The Name
attribute of the Table
element indicates the name of the table and the Member
attribute indicates that the data context has a member named Messages
that corresponds to this table. With in 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 have several children 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
. 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.
Message
object to the columns in Message
table. This Xml mapping is saved as an embedded resource in 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:Here is the code for the MessageBoardDataContext
class:
/// Data context for the message board public class MessageBoardDataContext : DataContext { /// Create a data context that uses the connection string /// specified in the configuration file public MessageBoardDataContext() : this(_connectionString) { } /// Create a data context for a specific connection string public MessageBoardDataContext(string connectionString) : base(connectionString, _mappingSource) { } // Default connection string read from the config file static string _connectionString = ConfigurationManager.ConnectionStrings[ "LocalSqlServer"].ConnectionString; //Initialize the mapping source read from the //XML file in the resource static XmlMappingSource _mappingSource = GetMappingSource(); private static XmlMappingSource GetMappingSource() { return XmlMappingSource.FromStream( typeof(MessageBoardDataContext) .Assembly .GetManifestResourceStream( "MessageBoard.DataAccess.Linq.Mapping.xml")); } /// Member that maps to the Messages table in the database public Table<Message> Messages { get { return GetTable<Message>(); } } }
As we already discussed LINQ to SQL has two different ways to map properties and fields in classes to columns in tables. The first one is via attributes specified on the properties and the classes and the second is through 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 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 the adding new messages to the database. Here is the implementation of the AddMessage
method:
public int AddMessage(string subject, string text, string postedBy, string postedById, DateTime datePosted) { using (MessageBoardDataContext context = CreateDataContext()) { context.ObjectTrackingEnabled = true; Message message = new Message(); message.Subject = subject; message.Text = text; message.PostedBy = postedBy; message.PostedById = postedById; message.DatePosted = datePosted; context.Messages.InsertOnSubmit(message); context.SubmitChanges(); //After calling submit changes the Id is automatically updated return message.Id; } }
After creating the DataContext
object the ObjectTrackingEnabled
property is set to true. What this means is that 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. The we call InsertOnSubmit
which indicates to the data context that a particular Message
objecthas 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 indicates 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 web site and a C# assembly. The web site consists of ASP.NET pages, style sheets and images. As far as possible the web site is coded declaratively. Any non-trivial code required to support the web site 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 a part of 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 web sites, 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 web site map:
The site has a master page named Site.master
which contains things like the header and the navigation bar. All pages in the web site use the same master page. The main page is Default.aspx
which shows list of all the messages with subject, user, date posted and partial text. When the user clicks on any of the message he is taken to the Message.aspx
page which shows the full details of the message. The site has 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
control. 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 screen shots of 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 an banner in the master page is quite common and nothing complex, the tricky part is to put the theme selector on the master page. The problem is that a web page theme can only be changed in the Page's PreInit
event and the master pages do not get applied until after it. In fact the master page is applied after the theme is set.
In order for the theme selector to work properly with the master page we have to rely on the Global.asax
file to change the theme as shown below:
void Application_PreRequestHandlerExecute(object sender, EventArgs e) { Page page = Context.Handler as Page; if (page == null) return; //Get the theme ... page.PreInit += delegate { page.Theme = theme; //Update the cookie ... }; }
The PreRequestHandlerExecute
execute function is called just before ASP.NET starts the page life cycle. The page object is instantiated but it's 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 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
. So 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 it's post-back value can be extracted from the Request.Forms
collection. So the following code in the master page ensures that the hidden field contains the unique id of the ThemeSelector
drop down list.
protected override void OnPreRender(EventArgs e) { base.OnPreRender(e); //Fill the theme selector drop down .... //Register an hiiden field that gives the ID of the selector control //as we don't know what it will be this.Page.ClientScript.RegisterHiddenField("ThemeSelectorId" , ThemeSelector.UniqueID); }
So 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 an 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 mainpage (
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 GridView, Repeater
and the DataList
controls. The best thing about the ListView
is that it offers 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 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 lot of expectations about ListView,
let's see if it meets the expectations.
Using ListView Control to Dipslay 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 object data source control as we have data available through the MessageSource
type. Remember that 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 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 theObjectDataSource
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
look like the following:
IEnumerable<Message> GetRecentMessages(int start, int count)
The start
parameter indicates the index first message to obtain from the list of all messages and the count
parameter indicates the maximum number of messages to obtain. Thus, theStartRowIndexParameterName
has been set to start and the MaximumRowsParameterName
hasbeen 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 liist 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 in the list view ocntrol.
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 id of messageList
and with-in 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 following:
<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
. TheListView
control replaces the place holder with the rendered HTML for each individual items 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 Eval
function. To make the code cleaner and also avoid reflection when using Data Binding, I have properties declared as following 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. Thus 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 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 and 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 ListView
control is that not only it can display data but it also has support for inserting and editing data. In case of 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 the InsertItemPosition
property to either "LastItem
" or to "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 points to observe here is how the subject and text fields are bound. The Text
property of Subject is set to Bind("Subject")
and that of 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 the parameter passed to Bind
(this is not a method or a function but just a special word understood by ASP.NET data binding engine), corresponds to the parameter names of AddMessage
. The other important thing to note here is the CommandName
property of thePost Message button. This indicates the 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 internet and catered to a global audience, you have to address the issue of time zone 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 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
- Displays 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 intutive.
- The option employed in the message board is to show the date and time in time zone of the user accessing the web site. 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 web site the users are given 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
object. 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 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); //Expire after a year 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); //It's ok just return Utc } return info; }
The above function extracts a time zone from the cookie if present or otherwise it returns TimeZoneInfo.Utc
which is the default. To display date and time information in users time zone, there is a separate function named GetFormattedTime
which returns a formatted date time value in the users 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 users 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 .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 web site. Of course, it makes 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 System.ServiceModel.Syndication
namespace. Why this is 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 web site, we use the syndication API in conjunction with the WCF Web programming model. Let's pause briefly to discuss about 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 web programming model you can issue simple HTTP GET or HTTP POST requests 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 plain HTTP GET request. - The two
ServiceKnownType
attributes ensure that the return valueSyndicationFeedFormatter
can either be a instance ofRss20FeedFormatter
orAtom10FeedFormatter
.
If a WCF method outputs an RSS or an ATOM feed, the return value of the method should be SyndicationFeedFormatter
(or one of it's sub classes). WCF will serialize SyndicationFeedFormatter
object output the raw RSS or the ATOM feed.
Next, we need to implement the interface is a class.
public class FeedService : IFeedService
Here are the steps to use 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 theItems
property of theSyndicationFeed.
Let's see these steps as implemented in the FeedService
.
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 the 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 recent 10 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, //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 of the 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, 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 Feed.svc
file in the Message Board web site. The contents of Feed.svc
are shown below:
<%@ ServiceHost Language="C#" Debug="true" Service="MessageBoard.Web.FeedService" %>
This make sure the our service is available through the Feed.svc
file in the web site. 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>
Important things to note here is that the service uses webHttpBinding
and the endpoint behavior includes webHttp
. Bothe of these are necessary for service to be accessed via web programming model. Finally, when you can access the feeds by typing in the appropriate URLs as shown in the screen shot below:
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 tow 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 outlook theme:
The theme tries to simulate Outlook 2007 Silver theme as much as possible. The background gradients are all made possible by using background images. As you will remember that 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 window where as the Posted By and Date Posted columns stay constant.
Finally, here is how the site looks in Floating theme:
There is a custom background image on each of the message 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 Explore 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 web site. We will see in part III of the series how we can add AJAX support to the message board web site.
Installation Instructions
The site needs either SQL Express or a full fledged SQL Server 2005.
If you have SQL Server Express, follow the following 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 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 if the connection string. Note: I have not included the full connection string here.
- Right click on the file
Install.ashx
which is in the message board web site project and click on view in browser.
- This will launch the browser and automatically create the database.
If you have SQL Server, follow the following steps:
- Open the solution
- Build the solution
- Open Install.sql file MessageBoard web site 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, and 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 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(); //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 exist. We first use this method to check if the database exist 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 to Series Navigation
License
This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)
No comments:
Post a Comment