|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Table of Contents
IntroductionNote: 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 ArticleThe article introduces the following new features in VS 2008:
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 ApplicationThe 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:
Here are some features of the message board as implemented in this article:
Message Board Architecture and DesignWith 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 LayerThe 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. MessageSince we have a message board website and the message board consists of messages, we need some way to represent a message. The
Each message has an The final property which needs explanation is the 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 IMessageProviderThere 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,
The diagram above shows the
Let's look at the methods in the
The rationale behind returning MessageSourceSo, we have a The
The methods in the 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 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 The other methods, 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 Data Access LayerThe 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 The Given this database table, it is pretty easy to implement an
For example, here is how the implementation of 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 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 const string GETRECENTMESSAGESSQL =
"EXEC GetRecentMessages @Id, @start, @count";
The above SQL looks a little simpler, but has no impact on the implementation of the 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 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 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
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 SQLYou 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
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 <?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 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 /// Data context for the message board
public class MessageBoardDataContext : DataContext
{
/// Create a data context that uses the connection string
/// specified in the configuration file
public MessageBoardDataContext()
: this(_connectionString)
{
}
/// Create a data context for a specific connection string
public MessageBoardDataContext(string connectionString)
: base(connectionString, _mappingSource)
{
}
// Default connection string read from the config file
static string _connectionString
= ConfigurationManager.ConnectionStrings[
"LocalSqlServer"].ConnectionString;
//Initialize the mapping source read from the
//XML file in the resource
static XmlMappingSource _mappingSource
= GetMappingSource();
private static XmlMappingSource GetMappingSource()
{
return XmlMappingSource.FromStream(
typeof(MessageBoardDataContext)
.Assembly
.GetManifestResourceStream(
"MessageBoard.DataAccess.Linq.Mapping.xml"));
}
/// Member that maps to the Messages table in the database
public Table<Message> Messages
{
get
{
return GetTable<Message>();
}
}
}
As we already discussed, LINQ to SQL has two different ways to map properties and fields in classes to columns in tables. The first one is via attributes specified on the properties and the classes, and the second is through an XML file. LINQ to SQL has a general purpose abstract base class called That's all! The 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 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 <Column Name="Id" Member="Id" DbType="
Int NOT NULL IDENTITY" IsPrimaryKey="true"
IsDbGenerated="true"
AutoSync="OnInsert" />
The 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 Why
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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.
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.
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.
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:"