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 LINQ with ASP.NET

Using DLINQ with ASP.NET

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

 

 

 

Published Wednesday, July 19, 2006 12:20 AM by ScottGu

Comments

# Building a Photo Tagging Application using ASP.NET 2.0, LINQ, and Atlas

Wednesday, July 19, 2006 4:48 AM by ScottGu's Blog

Over the last few days I&amp;rsquo;ve spent some spare time playing around with LINQ and LINQ for SQL (aka

# Building a Photo Tagging Application using ASP.NET 2.0, LINQ, and Atlas

Wednesday, July 19, 2006 6:07 AM by ナオキにASP.NET(仮)

ScottGu's Blog&amp;nbsp;からです。 &amp;nbsp; 今回のスコットさんの投稿はかなり濃いです。 &amp;nbsp; Building a Photo Tagging Application...

# re: Building a Photo Tagging Site using ASP.NET 2.0, LINQ, and Atlas

Wednesday, July 19, 2006 6:24 AM by Boris Yeltsin
This is really great. A full example with updates and inserts :) I'm still trying to figure out how you get the "SubmitChanges" method to appear. There must be a difference somewhere which makes your project files work, when this method does not seem to appear when you try to write this project from scratch using the "LINQ ASP.NET" project type...

# re: Building a Photo Tagging Site using ASP.NET 2.0, LINQ, and Atlas

Wednesday, July 19, 2006 6:42 AM by Boris Yeltsin
Scott, Have you tried working with any of these LINQ samples in VB.NET? The problem I find with all of them is that VS constantly tries to re-parse the SqlMetal-generated file in the App_Code folder. This makes it almost impossible to work as references are constantly appearing and disappearing, and getting it to build is game of chance :) Any idea how to stop it constantly parsing? (50% of the times it parses it it fails with a bunch of errors in the Error List panel)

# Building a Photo Tagging Site using ASP.NET 2.0, LINQ, and Atlas

Wednesday, July 19, 2006 7:15 AM by DotNetKicks.com

You've been kicked (a good thing) - Trackback from DotNetKicks.com

# re: Building a Photo Tagging Site using ASP.NET 2.0, LINQ, and Atlas

Wednesday, July 19, 2006 9:02 AM by kevin
That was a bad ass tutorial. I was really suprised to see the new syntax structure in C# 3.0 (I assume) - i'm excited to dive into that that when it's available (reminds me of oop javascript). Since LINQ is in CTP is it worth taking a look at? When do you estimate a version 1.0 release? hmm..I wonder if the nhibernate community will switch over to LINQ once it's v1 comes out. ok, thanks again.

# re: Building a Photo Tagging Site using ASP.NET 2.0, LINQ, and Atlas

Wednesday, July 19, 2006 9:08 AM by ejadib
Leyendo el blog de Scott Gu (que se los recomiendo!), me encontre con una aplicacion que utiliza LINQ, DLINQ y Atlas....

# re: Building a Photo Tagging Site using ASP.NET 2.0, LINQ, and Atlas

Wednesday, July 19, 2006 9:21 AM by Steve
Very cool sample! Thanks for the post!

# re: Building a Photo Tagging Site using ASP.NET 2.0, LINQ, and Atlas

Wednesday, July 19, 2006 10:52 AM by ScottGu

Hi Boris,

The intellisense engine currently struggles at times with completing a few of the statements (although compilation fully works).

The SubmitChanges() method is one I have also noticed does not always complete -- even though it is there and works on the DataContext object.

I haven't tried the sample yet in VB - although in theory it should work fine.  Can you send me details via email about the design-time scenario you are seeing failures with?  I'll then loop you in with the LINQ team who can take a look at.

Hope this helps,

Scott

# re: Building a Photo Tagging Site using ASP.NET 2.0, LINQ, and Atlas

Wednesday, July 19, 2006 10:54 AM by ScottGu

Hi Kevin,

I'd recommend spending a little time playing with LINQ over the next few months to get a feel for it and think about some of the possibilities it provides (which are almost endless).

We will support a go-live program of it before it RTM's -- so it won't be too long now before you can safely deploy projects with it.

Thanks,

Scott  

# re: Building a Photo Tagging Site using ASP.NET 2.0, LINQ, and Atlas

Wednesday, July 19, 2006 11:15 AM by Pepe Ramirez
Great article. You make it easy! cheers ~ pepe

# re: Building a Photo Tagging Site using ASP.NET 2.0, LINQ, and Atlas

Wednesday, July 19, 2006 4:48 PM by Alan Le
Good article. This sample is the first time I saw LINQ and I'm quite impressed. Looking forward to it in C# 3.0 and VS2005 tie-in. btw, there are two typos in the download readme.txt. "PhotosDB" should be PhotoDB. and "Populate Database" is missing the "L".

# re: Building a Photo Tagging Site using ASP.NET 2.0, LINQ, and Atlas