ASP.NET Social Networks—Blogs in Fisharoo

Exclusive offer: get 50% off this eBook here
ASP.NET 3.5 Social Networking

ASP.NET 3.5 Social Networking — Save 50%

An expert guide to building enterprise-ready social networking and community applications with ASP.NET 3.5

$29.99    $15.00
by Andrew Siemer | December 2008 | .NET Microsoft MySQL PHP

In this article by Andrew Siemer, we will talk about the concept of Blogging and how it could be applied to our community site Fisharoo. With search engines, users, and security in mind, Andrew invests a part of this article to address an issue that plagues many dynamic websites—query string data being used to determine page output.

The article starts with the discussion of Problem, that is, what we need to do to achieve success for the article's topic-Blogs so to speak. Then it moves on to Design where we decide on and write down the physical requirements for our feature. And finally comes the Solution part where we discuss how to implement all the requirements for each feature.

Problem

This article, as stated in Introduction, is all about adding the Blogging feature to our site. This will handle creating and managing a post. It will also handle sending alerts to your friends' filter page. And finally we will handle creating a friendly URL for your blog posts. Here we are making our first post to our blog:

ASP.NET Social Networks—Blogs in Fisharoo

Once our post is created, we will then see it on the Blogs homepage and the My Posts section. From here we can edit the post or delete it. Also, we can click into the post to view what we have seen so far.

ASP.NET Social Networks—Blogs in Fisharoo

The following screenshot shows what one will see when he/she clicks on the post:

ASP.NET Social Networks—Blogs in Fisharoo

I have the blog post set up to show the poster's avatar. This is a feature that you can easily add to or remove. Most of your users want to be able to see who the author is that they are currently reading!

Also, we will add a friendly URL to our blog post's pages.

ASP.NET Social Networks—Blogs in Fisharoo

Design

The design of this application is actually quite simple. We will only need one table to hold our blog posts. After that we need to hook our blog system into our existing infrastructure.

Blogs

In order for us to store our blog, we will need one simple table. This table will handle all the standard attributes of a normal blog post to include the title, subject, page name, and the post itself. It has only one relationship out to the Accounts table so that we know who owns the post down the road. That's it!

ASP.NET Social Networks—Blogs in Fisharoo

Solution

Let's take a look at the solution for these set of features.

Implementing the database

Let's take a look at the tables required by our solution.

Blogs

The blogs table is super simple. We discussed most of this under the Blogs section.

ASP.NET Social Networks—Blogs in Fisharoo

The one thing that is interesting here is the Post column. Notice that I have this set to a varchar(MAX) field. This may be too big for your community, so feel free to change it down the road. For my community I am not overly worried. I can always add a UI restriction down the road without impacting my database design using a validation control. After that we will look at the IsPublished flag. This flag tells the system whether or not to show the post in the public domain. Next to that we will also be interested in the PageName column. This column is what we will display in the browser's address bar. As it will be displayed in the address bar, we need to make sure that the input is clean so that we don't have parsing issues (responsible for causing data type exceptions) down the road. We will handle that on the input side in our presenter later.

Creating the relationships

Once all the tables are created, we can then create all the relationships. For this set of tables we have relationships between the following tables:

  • Blogs and Accounts

Setting up the data access layer

To set up the data access layer follow the steps mentioned next:

  • Open the Fisharoo.dbml file.
  • Open up your Server Explorer window.
  • Expand your Fisharoo connection.
  • Expand your tables. If you don't see your new tables try hitting the Refresh icon or right-clicking on tables and clicking Refresh.
  • Then drag your new tables onto the design surface.
  • Hit Save and you should now have the following domain objects to work with!

ASP.NET Social Networks—Blogs in Fisharoo

Keep in mind that we are not letting LINQ track our relationships, so go ahead and delete them from the design surface. Your design surface should have all the same items as you see in the screenshot (though perhaps in a different arrangement!).

Building repositories

With the addition of new tables will come the addition of new repositories so that we can get at the data stored in those tables. We will be creating the following repository to support our needs.

  • BlogRepository

Our repository will generally have a method for select by ID, select all by parent ID, save, and delete. We will start with a method that will allow us to get at a blog by its page name that we can capture from the browser's address bar.

public Blog GetBlogByPageName(string PageName, Int32 AccountID)
{
Blog result = new Blog();
using(FisharooDataContext dc = _conn.GetContext())
{
result = dc.Blogs.Where(b => b.PageName == PageName &&
b.AccountID == AccountID).FirstOrDefault();
}
return result;
}

Notice that for this system to work we can only have one blog with one unique page name. If we forced our entire community to use unique page names across the community, we would eventually have some upset users. We want to make sure to enforce unique page names across users only for this purpose. To do this, we require that an AccountID be passed in with the page name, which gives our users more flexibility with their page name overlaps! I will show you how we get the AccountID later. Other than that we are performing a simple lambda expression to select the appropriate blog out of the collection of blogs in the data context.

Next, we will discuss a method to get all the latest blog posts via the GetLatestBlogs() method. This method will also get and attach the appropriate Account for each blog. Before we dive into this method, we will need to extend the Blog class to have an Account property.

To extend the Blog class we will need to create a public partial class in the Domain folder.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Fisharoo.FisharooCore.Core.Domain
{
public partial class Blog
{
public Account Account { get; set; }
}
}

Now we can look at the GetLatestBlogs() method.

public List<Blog> GetLatestBlogs()
{
List<Blog> result = new List<Blog>();
using(FisharooDataContext dc = _conn.GetContext())
{
IEnumerable<Blog> blogs = (from b in dc.Blogs
where b.IsPublished
orderby b.UpdateDate descending
select b).Take(30);
IEnumerable<Account> accounts =
dc.Accounts.Where(a => blogs.Select(b =>
b.AccountID).Distinct().Contains(a.AccountID));
foreach (Blog blog in blogs)
{
blog.Account = accounts.Where(a => a.AccountID ==
blog.AccountID).FirstOrDefault();
}
result = blogs.ToList();
result.Reverse();
}
return result;
}

The first expression in this method gets the top N blogs ordered by their UpdateDate in descending order. This gets us the newest entries. We then add a where clause looking for only blogs that are published.

We then move to getting a list of Accounts that are associated with our previously selected blogs. We do this by selecting a list of AccountIDs from our blog list and then doing a Contains search against our Accounts table. This gives us a list of accounts that belong to all the blogs that we have in hand.

With these two collections in hand we can iterate through our list of blogs and attach the appropriate Account to each blog. This gives us a full listing of blogs with accounts.

As we discussed earlier, it is very important for us to make sure that we keep the page names unique on a per user basis. To do this we need to have a method that allows our UI to determine if a page name is unique or not. To do this we will have the CheckPageNameIsUnique() method.

public bool CheckPageNameIsUnique(Blog blog)
{
blog = CleanPageName(blog);
bool result = true;
using(FisharooDataContext dc = _conn.GetContext())
{
int count = dc.Blogs.Where(b => b.PageName == blog.PageName
&& b.AccountID == blog.AccountID).Count();
if(count > 0)
result = false;
}
return result;
}

This method looks at all the blog entries except itself to determine if there are other blog posts with the same page name that are also by the same Account. This allows us to effectively lock down our users from creating duplicate page names. This will be important down the road when we start to discuss our pretty URLs.

Next, we will look at a private method that will help us clean up these page name inputs. Keep in mind that these page names will be displayed in the browser's address bar and therefore need not have any characters in them that the browser would want to encode. While we can decode the URL easily, this conversation is more about keeping the URL pretty so that the user and search engine spiders can easily read where they are at. When we have characters in the URL that are encoded, we will end up with something like %20 where %20 is the equivalent to a space. But to read my%20blog%20post is not that easy. It is much easier to ready my-blog-post. So we will strip out all of our so called special characters and replace all spaces with hyphens. This method will be the CleanPageName() method.

private Blog CleanPageName(Blog blog)
{
blog.PageName = blog.PageName.Replace(" ", "-").Replace("!", "")
.Replace("&", "").Replace("?", "").Replace(",", "");
return blog;
}

You can add to this as many filters as you like. For the time being I am replacing the handful of special characters that we have just seen in the code. Next, we will get into the service layers that we will use to handle our interactions with the system.

ASP.NET 3.5 Social Networking An expert guide to building enterprise-ready social networking and community applications with ASP.NET 3.5
Published: December 2008
eBook Price: $29.99
Book Price: $49.99
See more
Select your format and quantity:

Implementing the services/application layer

Once all the repositories are built for single serving purposes, we can begin to create the services layer. Again, this layer is responsible for assembling aggregates and performing complex actions with our entities. We will not be creating any new services for this component but will need to add to the following existing service:

  • AlertService

AlertService

The AlertService is responsible for sending out notifications to our users via their filter page. This is the page that shows new activity amongst your profile and your friends' profiles.

public void AddNewBlogPostAlert(Blog blog)
{
alert = new Alert();
alert.CreateDate = DateTime.Now;
alert.AccountID = _userSession.CurrentUser.AccountID;
alert.AlertTypeID = (int)AlertType.AlertTypes.NewBlogPost;
alertMessage = "<div class="AlertHeader">" +
GetProfileImage(_userSession.CurrentUser.AccountID)
+ GetProfileUrl(_userSession.CurrentUser.Username)
+ " has just added a new blog post: <b>" +
blog.Title + "</b></div>";
alert.Message = alertMessage;
SaveAlert(alert);
SendAlertToFriends(alert);
}

Now we will quickly take a look at the AddNewBlogPostAlert() method. This method will take in the new Blog that was posted so that we can use some information about it in our alert. As soon as we get into the body of our method, we want to initialize a new Alert. We will then fill out some of the initial properties.

One of the properties that we will need to extend is the AlertType class that has the AlertTypes property. You will see that this AlertTypes property is really a representation of the record IDs we have stored in the AlertTypes table in the database. Open up the AlertType class and add a couple of new entries for the NewBlogPost and UpdatedBlogPost.

public partial class AlertType
{
public enum AlertTypes
{
AccountCreated = 1,
ProfileCreated = 2,
AccountModified = 3,
ProfileModified = 4,
NewAvatar = 5,
AddedFriend = 6,
AddedPicture = 7,
FriendAdded = 8,
FriendRequest = 9,
StatusUpdate = 10,
NewBlogPost = 11,
UpdatedBlogPost = 12
}
}

Then go into the AlertTypes table and create two new records-NewBlogPost and UpdatedBlogPost. If the record IDs that are generated do not correspond to the numbers you see above,update the numbers you see in the enum to the ones that were created in the table.

Now we can look at the message that we want to show in our alert. This can consist of any standard HTML as it will be displayed on the alerts page or "the filter" as we will call it. Once the Alert is fully configured, we can then save the Alert to the database. And in this case (but not all cases) we want this alert to show up on all of our friends' filters as well, to let them know that a new blog post was just created.

Here is the method for the updated blog post, which is almost identical to the one seen for updating the blog post:

public void AddUpdatedBlogPostAlert(Blog blog)
{
alert = new Alert();
alert.CreateDate = DateTime.Now;
alert.AccountID = _userSession.CurrentUser.AccountID;
alert.AlertTypeID = (int)AlertType.AlertTypes.NewBlogPost;
alertMessage = "<div class="AlertHeader">" +
GetProfileImage(_userSession.CurrentUser.AccountID)
+ GetProfileUrl(_userSession.CurrentUser.Username)
+ " has updated the <b>" + blog.Title +
"</b> blog post!</div>";
alert.Message = alertMessage;
SaveAlert(alert);
SendAlertToFriends(alert);
}

With the repository and service layers completed, we can now take a look at our UI.

Implementing the presentation layer

The presentation layer is almost as simple as the infrastructure is. We will have a Blog link in the top navigation. When you click on this link you will be taken to a page where you see a list of the latest blog posts. From the Blog section you can then choose to either view a blog from the latest blogs page or you can choose to view your blogs or create a new blog. In addition to these four pages we will also address how fancy URLs come into play in the view post page.

Latest blog posts

Viewing the latest blog posts is a single call to the BlogRepository. In order for our UI to get to any repository though, it first has to hand off its control of all display interactions. It does this by initializing an instance of the DefaultPresenter and then passing a reference to itself into the Init() method of the presenter. Once in the Init() method of the presenter we can then make the call into the repository to get the latest blogs.

public class DefaultPresenter
{
private IDefault _view;
private IBlogRepository _blogRepository;
public DefaultPresenter()
{
_blogRepository =
ObjectFactory.GetInstance<IBlogRepository>();
}

public void Init(IDefault View)
{
_view = View;
_view.LoadBlogs(_blogRepository.GetLatestBlogs());
}
}

With the latest blogs in hand we are then able to pass them into the UI's code behind where the blogs are bound to a list view control in the Default.aspx page.

public partial class Default : System.Web.UI.Page, IDefault
{
private DefaultPresenter _presenter;
public Default()
{
_presenter = new DefaultPresenter();
}
protected void Page_Load(object sender, EventArgs e)
{
_presenter.Init(this);
}
public void LoadBlogs(List<Blog> Blogs)
{
lvBlogs.DataSource = Blogs;
lvBlogs.DataBind();
}
public void lvBlogs_ItemDataBound(object sender,
ListViewItemEventArgs e)
{
Literal litBlogID = e.Item.FindControl("litBlogID") as
Literal;
HyperLink linkTitle = e.Item.FindControl("linkTitle") as
HyperLink;
Literal litPageName = e.Item.FindControl("litPageName") as
Literal;
Literal litUsername = e.Item.FindControl("litUsername") as
Literal;
//linkTitle.NavigateUrl = "~/Blogs/ViewPost.aspx?BlogID=" +
litBlogID.Text;
linkTitle.NavigateUrl = "~/Blogs/" + litUsername.Text + "/" +
litPageName.Text + ".aspx";
}
}

Also note that we have the ItemDataBound() method to handle each item that is bound. This will allow us to configure all the controls in the UI for each set of objects. Notice in particular that at the very end of our ItemDataBound() method, that we are configuring the NavigateUrl property to display fancy URLs! This will be important to remember when we start our discussion about the ViewPost.aspx page.

Out in the UI side we can see how all the data sections are bound to the container's blog items.

<asp:ListView ID="lvBlogs" runat="server" 
OnItemDataBound="lvBlogs_ItemDataBound">
<LayoutTemplate>
<ul class="blogsList">
<asp:PlaceHolder ID="itemPlaceholder"
runat="server"></asp:PlaceHolder>
</ul>
</LayoutTemplate>

<ItemTemplate>
<li>
<h2 class="blogsTitle"><asp:HyperLink ID="linkTitle"
runat="server" Text='<%#((Blog)
Container.DataItem).Title %>'>
</asp:HyperLink></h2>
<p class="blogsDescription">
Created: <%#((Blog)Container.DataItem).CreateDate %> By:
<%#((Blog)Container.DataItem).
Account.Username %><br />
<%#((Blog)Container.DataItem).Subject %><asp:Literal
ID="litBlogID" runat="server" Text=
'<%#((Blog)Container.DataItem).BlogID %>'></asp:Literal>
<asp:Literal ID="litPageName" runat="server"
Visible="false" Text='<%#((Blog)
Container.DataItem).PageName %>'></asp:Literal>
<asp:Literal ID="litUsername" runat="server"
Visible="false" Text='<%#((Blog)
Container.DataItem).Account.Username
%>'></asp:Literal>
</p>
</li>
</ItemTemplate>

<EmptyDataTemplate>
Sorry, there are no blogs posted yet!
</EmptyDataTemplate>
</asp:ListView>

If we had data in the system we would now see a list of the latest blogs!

My blog posts

The My Blog Posts section is 99.999% identical to the latest blogs post with the exception that they load their list of Blog objects via a different call into the same BlogRepository. In this case we get a list of Blogs by calling the GetBlogsByAccountID() method. This method will get a list of our blog posts sorted by their create date.

Fancy URL support

Now that we have both the recent blog posts and the my posts pages created and out of the way, we need to discuss handling the pretty URLs that we have our UIs currently displaying. At the moment we are sending people to domain.com/blogs/username/pagename. As you may have guessed, this is a path to a resource that does not actually exist. In order to handle the unknown resources, we will have to extend the UrlRewrite class that we have in the Handler's directory.

public class UrlRewrite : IHttpModule
{
private IAccountRepository _accountRepository;
private IBlogRepository _blogRepository;
public UrlRewrite()
{
_accountRepository =
ObjectFactory.GetInstance<IAccountRepository>();
_blogRepository =
ObjectFactory.GetInstance<IBlogRepository>();
}
public void Init(HttpApplication application)
{
//let's register our event handler
application.PostResolveRequestCache +=
(new EventHandler(this.Application_OnAfterProcess));
}
public void Dispose()
{

}
private void Application_OnAfterProcess(object source,
EventArgs e)
{
HttpApplication application = (HttpApplication)source;
HttpContext context = application.Context;
string[] extensionsToExclude = { ".axd", ".jpg", ".gif",
".png", ".xml", ".config", ".css", ".
js", “.aspx”, ".htm", ".html" };
foreach (string s in extensionsToExclude)
{
if
(application.Request.PhysicalPath.ToLower().Contains(s))
return;
}
if (!System.IO.File.Exists(application.Request.PhysicalPath))
{
if
(application.Request.PhysicalPath.
ToLower().Contains("blogs"))
{
string[] arr =
application.Request.PhysicalPath.
ToLower().Split('');
string blogPageName = arr[arr.Length - 1];
string blogUserName = arr[arr.Length - 2];
blogPageName = blogPageName.Replace(".aspx", "");
if (blogPageName.ToLower() != "profileimage" &&
blogUserName.ToLower() != "profileavatar")
{
Account account =
_accountRepository.
GetAccountByUsername(blogUserName);
Blog blog =
_blogRepository.GetBlogByPageName
(blogPageName, account.AccountID);
context.RewritePath("~/blogs/ViewPost.aspx?BlogID=" +
blog.BlogID.ToString());
}
else
{
return;
}
}
else
{
string username =
application.Request.Path.Replace("/", "");
Account account =
_accountRepository.GetAccountByUsername(username);
if (account != null)
{
string UserURL =
"~/Profiles/profile.aspx?AccountID="
+ account.AccountID.ToString();
context.Response.Redirect(UserURL);
}
else
{
context.Response.Redirect("~/PageNotFound.aspx");
}
}
}
}
}

In the UrlRewrite class just seen, notice that I added a reference to the BlogRepository so that we can get the blog in question if that is indeed what this rewrite is for. Next, notice that I removed the .aspx extension from the list of extensions to exclude it from processing. This is because we want our pages to look like real pages even though they are actually dynamic (read non-existent) resources.

After that we test to see if we are working with a blog redirection. If we are, then we extract the user's username and the page name from the URL. With this information in hand we can locate the Blog that we need. From there we can easily do a redirection to the page as though the user had no idea. To them the pretty URL stays intact just as it was when they entered it or followed it.

Now when rewriting the URL on the server side you have to be aware that the local path "~" identifier may no longer work as expected. In my case it makes all images load as though the blog directory is the root directory. So for this reason you will notice that the ViewPost.aspx page has items in its UI with root level mappings in the standard HTML fashion /images/resource rather than ~/images/resource. This fixes the issue without any problem. Everything else should work as expected.

View post

The ViewPost.aspx page is an amazingly straightforward page to build. It is extracting the page to be viewed from the URL by way of the rewritten URL, which contains the BlogID behind the scenes. The ViewPostPresenter gets to the BlogID through the WebContext.BlogID property.

_view.LoadPost(_blogRepository.GetBlogByBlogID(_webContext.BlogID));

Here is the UI that we are loading:

<h2><asp:Label ID="lblTitle" runat="server"></asp:Label></h2>
<asp:HyperLink ID="linkProfile" runat="server">
<asp:Image style="padding-bottom:5px;float:left;" Width="200"
Height="200" ID="imgAvatar" runat="server" ImageUrl="/images/
profileavatar/profileimage.aspx" />
</asp:HyperLink>
Created: <asp:Label ID="lblCreated" runat="server"></asp:Label>
Updated: <asp:Label ID="lblUpdated" runat="server"></asp:Label><br
/><br />
<asp:Label ID="lblPost" runat="server"></asp:Label>

The thing to be pay attention to, as I mentioned before, is that all the paths are in a fixed format off the root of the site. This way no matter where we are at, we know where to go to gain access to the specified resource.

Create or edit post

With all of this work out of our way we can now turn our attention to the dirty work of creating the actual blog post. This page will actually serve two purposes. We need to use it to create our blog post. But we will also repurpose the UI to edit already existing posts as well. Let's look at the presenter for this page:

public void Init(IPost View)
{
_view = View;
if(_webContext.BlogID > 0)
{
_view.LoadPost(_blogRepository.GetBlogByBlogID(_webContext.BlogID));
}
}
public void SavePost(Blog blog)
{
bool result = _blogRepository.CheckPageNameIsUnique(blog);
if (result)
{
blog.AccountID = _webContext.CurrentUser.AccountID;
_blogRepository.SaveBlog(blog);
}
else
{
_view.ShowError("The page name you have chosen is in use.
Please choose a different page name!");
}
}

In the first section of the Init method we are checking the WebContext.BlogID property to see if we have something to work with. If we do then we load the UI with the appropriate blog. The next item you see is the SavePost method that takes care of passing a loaded blog into the BlogRepository to be saved.

Next, we will take a look at the code behind that the presenter works with. Here we will see the LoadPost and btnSave_Click methods. There is nothing fancy to follow here. But the one aspect to pay attention to is that we keep track of the BlogID in the page so that we know what we are working with later.

protected void btnSave_Click(object sender, EventArgs e)
{
Blog blog = new Blog();
if (litBlogID.Text != "")
blog.BlogID = Convert.ToInt64(litBlogID.Text);
blog.IsPublished = chkIsPublished.Checked;
blog.PageName = txtPageName.Text;
blog.Post = txtPost.Text;
blog.Subject = txtSubject.Text;
blog.Title = txtTitle.Text;
_presenter.SavePost(blog);
}
public void LoadPost(Blog blog)
{
txtTitle.Text = blog.Title;
txtSubject.Text = blog.Subject;
txtPost.Text = blog.Post;
txtPageName.Text = blog.PageName;
chkIsPublished.Checked = blog.IsPublished;
litBlogID.Text = blog.BlogID.ToString();
}

This takes care of the little details of data inputs and outputs. However, let's now take a look at what we need to do make the UI somewhat useable. We will make use of Xinha WYSIWYG editor. All that is required to hook this up is a multiline text box control and a single line of JavaScript.

...
<asp:TextBox TextMode="MultiLine" ID="txtPost"
runat="server"></asp:TextBox>
...
<script type="text/javascript">
xinha_editors[xinha_editors.length] = 'ctl00_Content_txtPost';
</script>
...

With this in place we should be ready to create a blog post!

ASP.NET Social Networks—Blogs in Fisharoo

Summary

In this article we covered the creation of blog posts. We also went over the pages that are needed for people to see other users' posts as well as their own. And of course we provided a page to actually read a post. In addition to this we covered the concept of fancy or pretty URLs that are more user as well as search engine friendly. Finally, we added a touch more usability to our UI in the form of the Xinha WYSIWYG.

ASP.NET 3.5 Social Networking An expert guide to building enterprise-ready social networking and community applications with ASP.NET 3.5
Published: December 2008
eBook Price: $29.99
Book Price: $49.99
See more
Select your format and quantity:

About the Author :


Andrew Siemer

Andrew Siemer is currently the enterprise architect at OTX Research. He has worked as a software engineer, enterprise architect, trainer, and author since 1998 when he got out of the Army. Andrew has consulted with many companies on the topics of e-commerce, social networking, and business systems. To name a few, he has worked with eUniverse (AllYouCanInk.com), PointVantage (MyInks.com), Callaway Golf (CallawayConnect.com), Guidance Software (GuidanceSoftware.com), and Intermix Media (Grab.com, AmericanIdol.com, FoxSports.com, FlowGo.com). In addition to his daily duties he also offers various classes in .NET, C#, and other web technologies to local students in his area as well as blogging in his *free* time.

Books From Packt

  ASP.NET 3.5 Application Architecture and Design
ASP.NET 3.5 Application Architecture and Design

Beginners Guide to SQL Server Integration Services Using Visual Studio 2005
Beginners Guide to SQL Server Integration Services Using Visual Studio 2005

Microsoft AJAX Library Essentials: Client-side ASP.NET AJAX 1.0 Explained
Microsoft AJAX Library Essentials: Client-side ASP.NET AJAX 1.0 Explained

ASP.NET Data Presentation Controls Essentials
ASP.NET Data Presentation Controls Essentials

Software Testing with Visual Studio Team System 2008
Software Testing with Visual Studio Team System 2008

Entity Framework Tutorial
Entity Framework Tutorial

LINQ Quickly
LINQ Quickly

Microsoft Visual C++ Windows Applications by Example
Microsoft Visual C++ Windows Applications by Example

Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software