Building a Photo Tagging Site using ASP.NET 2.0, LINQ, and Atlas
Over the last few days I’ve spent some spare time playing around with LINQ and LINQ for SQL (aka DLINQ) – both of which are incredibly cool technologies.
Specifically, I’ve been experimenting with building a photo management application that provides “tagging” support. If you aren’t familiar with photo tagging, you might want to check out FlickR – which implements a tagging mechanism that enables users to easily annotate pictures with textual words that provide a way to easily organize and sort them.
For my photo-browser I wanted to implement a “tag cloud” on every page of the site that lists the most popular tags in use on the left-hand side, and enables users to be able to click a tag within the "tag cloud" to easily filter the pictures by them:
When a user clicks on an individual picture I then wanted them to see a more detailed version of the picture, as well as all the tags it has been annotated with:
I then wanted end-users to be able to click on the “edit tags” link on the page to switch into an edit mode using Ajax, where they can easily create or edit new tags for the picture. Any word can be suggested:
When the tags are saved, I wanted the “tag cloud” to dynamically update with the current list of tags being used across all photos on the site, and size each tag within the cloud based on the tag’s frequency (higher usage frequency produces bigger font sizes for that tag):
It turns out that implementing the above solution is very easy using ASP.NET 2.0, LINQ and Atlas. The below post walks through a simple sample to illustrate the basics. You can also download a completed version of the sample to try out on your own (details on how to-do this are at the end of this post).
Step 1: Creating our Database
I started the application by first creating a database within SQL Server to model albums, photos, and photographers, as well as tag annotations mapped against them.
Here is the schema I ended up using for this particular sample (I’ll be adding more properties as I expand the photo management app more in future posts – but this is a basic start):
You’ll notice that I have an “Albums” and “Photos” table to store common metadata associated with pictures. I am also using a Photographers table to store photographer names. The Photos table has foreign-key (FK) relationships against both it and the Albums tables.
I then created a “Tags” table to track tag annotations for the photos. The Tags table has a foreign-key relationship to the Photos table, and maintains a separate row for each Tag used on the site. One benefit of this approach is that it makes adding new Tags super fast and easy (you don’t need to worry about duplicate tag-name insert collisions, and you can add and associate new tags in one database round-trip).
In addition to providing referential integrity, the foreign-key from the Tags table to the Photos table also enables cascading deletes – so that if a photo is deleted the tags associated will automatically be deleted too (avoiding cases of “dangling tags” left in the database that are no longer associated with the data they were associated against).
Step 2: Creating our LINQ for SQL data model
Once my database was built, I created a new LINQ-enabled ASP.NET web-site by selecting the LINQ template that is installed by the LINQ May CTP within the “New Website” dialog in Visual Web Developer (the free IDE for ASP.NET development). This sets up an ASP.NET 2.0 project that has LINQ fully configured, and allows me to easily use LINQ to connect against databases.
I then created a LINQ-enabled object model that I could use to interact with the database defined above. The next release of Visual Studio will provide a really nice WYSIWYG designer for creating and mapping this database object model. In the meantime, I just used the command-line “sqlmetal” utility that ships with LINQ to quickly create this. All I had to-do was to open up a command-line console and type the following commands to accomplish this:
>> cd c:\Program Files\LINQ Preview\Bin
>>
>> sqlmetal /database:PhotoDB /pluralize /namespace:PhotoAlbum /code:c:\Projects\PhotoApp\app_code\PhotoDB.cs
This created a LINQ-enabled object model for the “PhotoDB” database on my local system and stored the generated classes within the “PhotoDB.cs” file within the ASP.NET LINQ project. Note that the classes generated are declared as “partial types” – meaning developers can optionally add additional properties/methods to them in separate files (common scenarios: entity validation or helper methods).
Once this LINQ-enabled object model is created, I can then easily use this LINQ-enabled object model anywhere within my ASP.NET application. For example, to create two new Albums in the Albums table I could just write this code:
PhotoDB photoDb = new PhotoDB();
Album album1 = new Album();
album1.AlbumName = "Africa Trip";
Album album2 = new Album();
Album2.AlbumName = "Europe Trip";
photoDb.Albums.Add(album1);
photoDb.Albums.Add(album2);
photoDb.SubmitChanges();
When the “SubmitChanges()” method is called above, the album instances above are saved into the SQL Server database within the Albums table – without me having to write any raw SQL or data access code. The above code is all that needed to be written for this to work.
Step 3: Using LINQ to work with data and Tag Photos in our application
LINQ makes working with data a real joy. It automatically handles relationships, hierarchy, and tracking changes within our model – eliminating tons of data access code.
For example, to create a new album and a photo within it, I could extend our previous code-sample like this:
PhotoDB photoDb = new PhotoDB();
Photographer photographer1 = new Photographer();
photographer1.PhotographerName = "Scott Guthrie";
Photo photo1 = new Photo();
photo1.Description = "Picture of Lion";
photo1.Url = "http://someurl";
photo1.Thumbnail = "http://thumbnailurl";
photo1.Photographer = photographer1;
Album album1 = new Album();
album1.AlbumName = "Africa Trip";
album1.Photos.Add(photo1);
photoDb.Albums.Add(album1);
photoDb.SubmitChanges();
This is all of the code needed to add a new Photographer, Photo and Album into the database, setup the FK relationship between the Photographer and Photo, and setup the Photo and the Album FK relationship (notice how this relationship is expressed by adding the photo into the Album’s photo collection, and by setting the Photographer property on Photo). Because these properties and collections are strongly-typed, we get full compile-time checking and intellisense of our syntax and data relationships.
C# has also added new syntax support that can be used to make object initialization even terser/cleaner than what I did above. Specifically, it now allows developers to declare properties (and collections) using syntax like below if you prefer:
PhotoDB photoDb = new PhotoDB();
Photographer scott = new Photographer() {
PhotographerName = "Scott Guthrie"
};
photoDb.Albums.Add( new Album() {
AlbumName = "South Africa",
Photos = {
new Photo() {
Description = "Lion Close Up",
Photographer = scott,
Url = "http://url1",
Thumbnail = "http://thumb1",
},
new Photo() {
Description = "Zebras at Dusk",
Photographer = scott,
Url = " http://url2",
Thumbnail = " http://thumb2",
}
}
} );
photoDb.SubmitChanges();
This is syntactically equivalent to the code before – except that we are now able to compact more functionality in fewer lines of code. In the example above, I’m now adding two new photos to the new South Africa album (with me as the photographer for both pictures).
Because we setup FK relationships between the Photo table and the Tags table, we get automatic association linking between them with LINQ (this is expressed via the “Tags” property on Photos and the corresponding “Photo” property on each Tag). For example, I could use the below code to fetch one of our newly created Photo’s above from the database and associate three new Tags to it:
PhotoDB photoDB = new PhotoDB();
Photo photo = photoDB.Photos.Single(p => p.Description=="Lion Close Up");
photo.Tags.Add( new Tag() { Name="Lion" } );
photo.Tags.Add( new Tag() { Name="AndersH" } );
photo.Tags.Add( new Tag() { Name="ScottGu" } );
photoDb.SubmitChanges();
I could then use the below code to retrieve a Photo and output its tags within a page:
PhotoDB photoDB = new PhotoDB();
Photo photo = photoDB.Photos.Single(p => p.Description=="Lion Close Up");
foreach (Tag tag in photo.Tags) {
Response.Write("Tag : " + tag.Name);
}
I could also then write this code to easily retrieve all Photos that are tagged with a specific tag-name, and output the Photo description and photographer name for each of them:
PhotoDB photoDb = new PhotoDB();
string tagName = "Lion";
var photos = from photo in photoDb.Photos
where photo.Tags.Any(t => t.Name == tagName)
select photo;
foreach (Photo photo in photos) {
Response.Write("Photo: " + photo.Description + " by: " + photo.Photographer.PhotographerName);
}
I do not need to write any extra data code to make the above code work. LINQ handles all of the SQL statement execution for me. This provides an incredibly flexible and elegant way to perform data access.
Step 4: Adding “Tag Cloud” UI to our Application
After defining the database and creating the LINQ-enabled object-model above, I focused on the UI of the site.
To maintain a consistent layout and look and feel across the site, I first created an ASP.NET master page that I called “Site.Master”. Within this file I defined the basic layout structure that I wanted all pages to have, and used an external stylesheet to define CSS rules.
I then downloaded and added into my project a cool, free “Cloud Control” that Rama Krishna Vavilala built and published (with full source-code) in a nice article here. It encapsulates all of the functionality needed to render a list of weighted cloud tags within an ASP.NET page. It also supports standard ASP.NET databinding – which means I can easily bind a result from a LINQ query to it to output the correct weighted tags for our application.
My final Site.Master template to accomplish this ended up looking like this:
<%@ Master Language="C#" AutoEventWireup="true" CodeFile="Site.master.cs" Inherits="Site" %>
<%@ Register Namespace="VRK.Controls" TagPrefix="vrk" Assembly="VRK.Controls" %>
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<link href="StyleSheet.css" rel="stylesheet" type="text/css" />
</head>
<body>
<form id="form1" runat="server">
<div class="header">
<h1>Scott's Photo Sample</h1>
</div>
<div class="tagcloud">
<div class="subheader">
<h2>Filter Tags</h2>
</div>
<vrk:Cloud ID="Cloud1" runat="server"
DataTextField="Name"
DataTitleField="Weight"
DataHRefField="Name"
DataHrefFormatString="PhotoListing.aspx?tag={0}"
DataTitleFormatString="{0} photos"
DataWeightField="Weight" >
</vrk:Cloud>
</div>
<asp:contentplaceholder id="MainContent" runat="server">
</asp:contentplaceholder>
</form>
</body>
</html>
The below Site.Master code-behind file is then used to obtain a unique list of tags from the database (it uses the “data-projection” feature of LINQ to fetch a sequence of custom shaped types containing the Tag name and usage count). It then databinds this sequence to Rama’s control above like so:
using System;
using System.Query;
using PhotoAlbum;
public partial class Site : System.Web.UI.MasterPage {
void PopulateTagCloud() {
PhotoDB photoDb = new PhotoDB();
Cloud1.DataSource = from tag in photoDb.Tags
group tag by tag.Name into g
orderby g.Key
select new {
Name = g.Key,
Weight = g.Count()
};
Cloud1.DataBind();
}
protected void Page_Load(object sender, EventArgs e) {
PopulateTagCloud();
}
}
And now if I create an empty page based on the Site.Master above and hit it, I’ll automatically have the weighted tag-cloud added to the left-hand side of it:
Step 5: Browsing Photos By Tag
The next page I added to the site was one named “PhotoListing.aspx”. The tag-cloud control used above creates a hyperlink for each tag that links to this page and passes the tag name as an argument to it when you click a tag.
The PhotoListing.aspx page I created is based on the Site.Master template above and uses a templated DataList control to output the pictures in a two-column layout:
<asp:Content ID="C1" ContentPlaceHolderID="MainContent" Runat="server">
<div class="photolisting">
<asp:DataList ID="PhotoList" RepeatColumns="2" runat="server">
<ItemTemplate>
<div class="photo">
<div class="photoframe">
<a href='PhotoDetail.aspx?photoid=<%# Eval("PhotoId") %>'>
<img src='<%# Eval("Thumbnail") %>' />
</a>
</div>
<span class="description">
<%# Eval("Description") %>
</span>
</div>
</ItemTemplate>
</asp:DataList>
</div>
</asp:Content>
Below is the entire code-behind for the page:
using System;
using System.Query;
using PhotoAlbum;
public partial class PhotoListing : System.Web.UI.Page {
void PopulatePhotoList(string tagName) {
PhotoDB photoDb = new PhotoDB();
if (tagName == null) {
PhotoList.DataSource = photoDb.Photos;
}
else {
PhotoList.DataSource = from photo in photoDb.Photos
where photo.Tags.Any(t => t.Name == tagName)
select photo;
}
PhotoList.DataBind();
}
protected void Page_Load(object sender, EventArgs e) {
PopulatePhotoList( Request.QueryString["tag"] );
}
}
When a user clicks on the “Cats” tag in the tag-cloud, they’ll then see this list of photos rendered:
Step 6: Photo Details and Ajax Editing
The PhotoListing.aspx page above links each thumbnail image to a PhotoDetails.aspx page that shows the picture full-size, as well as lists out all of its tags. Users visiting the site can also optionally edit the tags using an inline Ajax-editing UI experience.
To implement the Ajax-UI I used the Atlas UpdatePanel control, and then nested an ASP.NET MultiView control within it. The Multiview control is a built-in control introduced in ASP.NET 2.0, and allows you to provide multiple “view” containers that can contain any HTML + server controls you want. You can then dynamically switch between the views within your code-behind page however you want. If the Multi-View control is nested within an Atlas UpdatePanel, then these view-switches will happen via Ajax callbacks (so no full-page refresh).
For the tag editing experience above I defined both “readView” and “editView” views within the Multiview control, and added “edit”, “cancel” and “save” link-buttons within them like so:
<atlas:UpdatePanel ID="p1" runat="server">
<ContentTemplate>
<asp:MultiView ID="MultiView1" runat="server" ActiveViewIndex="0">
<asp:View ID="readView" runat="server">
Tags:
<asp:Label id="lblTags" runat="server" />
<span class="photoBtn">
<asp:LinkButton ID="btnEdit" runat="server" OnClick="btnEdit_Click">[Edit Tags]</asp:LinkButton>
</span>
</asp:View>
<asp:View ID="editView" runat="server">
Tags:
<asp:TextBox ID="txtTags" runat="server" />
<span class="photoBtn">
<asp:LinkButton ID="btnSave" runat="server" OnClick="btnSave_Click">[Save Tags]</asp:LinkButton>
<asp:LinkButton ID="LinkButton1" runat="server" OnClick="btnCancel_Click">[Cancel]</asp:LinkButton>
</span>
</asp:View>
</asp:MultiView>
</ContentTemplate>
</atlas:UpdatePanel>
I then wired-up event-handlers for these 3 link-buttons in my code-behind like so:
protected void btnEdit_Click(object sender, EventArgs e) {
MultiView1.SetActiveView(editView);
}
protected void btnCancel_Click(object sender, EventArgs e) {
MultiView1.SetActiveView(readView);
}
protected void btnSave_Click(object sender, EventArgs e) {
UpdatePhoto(txtTags.Text);
}
The “save” event-handler above in turn calls the UpdatePhoto method and passes in the editView’s <asp:textbox> value as arguments. This method is defined within the code-behind like so:
void UpdatePhoto(string tagString) {
PhotoDB photoDb = new PhotoDB();
Photo photo = photoDb.Photos.Single(p => p.PhotoId == photoId);
photoDb.Tags.RemoveAll(photo.Tags);
photo.Tags = photoDb.TagWith(tagString, ' ');
photoDb.SubmitChanges();
}
The above method retrieves the specified Photo from the database, removes its current tags, and then uses the below “TagWith” helper method to create a new collection of tag instances to associate with the picture:
public EntitySet<Tag> TagWith (string tagNames, char separator) {
EntitySet<Tag> tags = new EntitySet<Tag>();
tagNames = tagNames.Trim();
foreach (string tagName in tagNames.Split(separator))
tags.Add(new Tag { Name = tagName });
return tags;
}
And with that I now have an editable Ajax-enabled editing experience for viewing and dynamically adding new Tags to my photos.
Summary
Hopefully the above post provides a good walkthrough of some of the really cool things you can do with LINQ, LINQ for SQL, ASP.NET 2.0 and Atlas.
You can download the completed sample here. Please review the “readme.txt” file in the root of the project to learn how to set it up and run it.
To learn more about using LINQ and LINQ for SQL with ASP.NET 2.0, please review these past three blog posts of mine as well:
Using DLINQ with Stored Procedures
Also make sure to check out the LINQ web-site here to download LINQ and start learning more about it (note: you need to install the May LINQ CTP build to run the sample).
Last but most importantly: I want to say a huge thank-you to Anders Hejlsberg and Matt Warren – who are not only responsible for the insanely cool technology in LINQ + DLINQ, but also very kindly spent some of their valuable time over the last few days educating me on how to best approach the app above, and in making some invaluable coding suggestions.
Hope this helps,
Scott