Home > .net 2.0, c# > Creating a Photo Gallery, Part #2

Creating a Photo Gallery, Part #2

January 19, 2007

In our quest to recreate a basic photo gallery, we’re ready to move from hard disk storage (bad) to database storage (good) and add a bit more extensibility to our photo album. In Part #1, we simply saved to and read from the hard disk and used the Image.GetThumbnailImage method to create thumbnail images on the fly. For concept, this works; however, is hardly manageable—especially as we progress further and need to add concepts such as galleries.

In this part, we’ll:

  1. FileUpload web control to ensure we’re only receiving GIF and JPG images
  2. Write to and read from a SQL Server 2005/SQL Express database,
  3. IHTTPHandler, to create a bit of reusability for our photo generation.

For a PDF version of this posting (for printing and code formatting, download it here: creatingaphotogallery2.pdf).  I recommend it (until I find a blogging engine that place nicely with code formatting again).

Checking for GIFs and JPGs

In our prior example, we somewhat assumed that the incoming images were either GIFs or JPGs and did not do any sort of verification. With saving into a SQL Database, we must verify the image type. Why? It gets down to streaming the data in and out of the database. Some image format, specifically PNG, require special handling. This is a functionality we can build in later, but, for now, we’ll just allow GIFs and JPGs.

This is accomplished by calling the ContentType property of our FileUpload object.

String contentType = uploader.PostedFile.ContentType;

Because we have various content types we want to allow, I recommend placing them into either an array, or in my example, a generic list. We’ll use this list to ensure we’re putting the correct content into the database.

List<String> validContentTypes = new List<String>();
validContentTypes.Add(“image/gif”);
validContentTypes.Add(“image/jpeg”);
validContentTypes.Add(“image/pjpeg”);

Keep that list object handy, we’ll use it later.

Reading and Writing to the Database

Before we begin coding, we need a simple table structure to hold our photos. Let’s start simple.

photogallery_part2_database.GIF

This can be as complex or as simple as you wish—we’ll go for simple in this case since database design isn’t the point of the tutorial.

Writing into the Database

To begin, we need to use our contentType string and our validContentTypes generic list we picked up earlier and determine if the FileUpload object has a valid file type.

if (validContentTypes.Exists(delegate(string type) { return contentType == type; }))

Why the odd delegate? Well, List<T>.Exists requires a predicate string<t>–you cannot simply pass a string object. The cool aspect? You can build this logic into a complex method in itself and just call the method (.Exists(myLogicalMethod)); however, we’ll do it inline here.

Rule #1 : Binary data requires a binary object of some sort to capture the streamed bytes into an array.

In this scenario, I chose a simple BinaryReader object to read the InputStream of our FileUpload object. It is then converted into a byte array, which is what we’ll be adding to our database. ReadBytes requires the length of the incoming content, which we can easily pull from the ContentLength property of FileUpload.

BinaryReader reader = new BinaryReader(uploader.PostedFile.InputStream);
byte[] image = reader.ReadBytes(uploader.PostedFile.ContentLength);

From here, a simple SQL insert statement is processed. For now, we’ll pre-populate our gallery_id, author_id, and description fields.

string sql =

“INSERT INTO Photos (photo, contenttype, gallery_id, description, author_id) ” +
“VALUES (@photo, @contenttype, @gallery_id, @description, @author_id)”;

using (

SqlConnection conn = new
SqlConnection(ConfigurationManager.ConnectionStrings[“PhotoStorage”].ConnectionString))

{

SqlCommand cmd = new SqlCommand(sql, conn);
cmd.Parameters.Add(“@photo”, SqlDbType.Image).Value = image;
cmd.Parameters.Add(“@contenttype”, SqlDbType.VarChar).Value = contentType;
cmd.Parameters.Add(“@gallery_id”, SqlDbType.Int).Value = 1;
cmd.Parameters.Add(“@description”, SqlDbType.VarChar).Value = “Test photo!”;
cmd.Parameters.Add(“@author_id”, SqlDbType.Int).Value = 1;
conn.Open();
int rowsChanged = cmd.ExecuteNonQuery();
conn.Close();

uploadStatus = “Upload succeeded for image of type ” + contentType + “!”;
return rowsChanged;

}

Remember, we’re in an if statement, our else statement can include a simple return message (of uploadStatus) giving the “wrong file type” failure.

else

{

uploadStatus = “Upload failed for image of type ” + contentType +

“. You can only upload .gif and .jpg images. Please convert your image and try

again.”;

return 0;

}

So, now the basics of our new method are complete. Part #1 called the PhotoStorage.UploadPhoto method; I’d recommend keeping that logic, but putting the information above into something like SqlUploadPhoto or whatever matches your naming conventions. Finally, go to your UploadPhoto.aspx’s code-behind and update the button call.

PhotoStorage.SqlUploadPhoto(sqlUploader, out uploadStatus);

Our SqlUploadPhoto method doesn’t return a thumbnail, but does return a success method. Again, very simplistic.

photogallery_part2_test1.GIF

Reading from the Database

Now, to read our images back out, we need to read from out database and display it to the user. I believe the simplest, and most reusable method, is to develop a custom HTTP handler, or endpoint, to handle these images. For more information on HTTP handlers, in general, view the Introduction to HTTP Handlers from MSDN.

To add a new handler, right-click on your solution or your web project, Add New Item. Choose the Generic Handler and give it a name. For this example, we’ll use SqlPhotoHandler.ashx.

photogallery_part2_newhandler.GIF

What does this do for us and what’s an .ashx file?

This allows us to render HTTP content back to the client by using the specific handler. I’m sure you have seen this technology in place in other locations like MSDN; but the point is that it’s transparent to the user.

An .ashx file is a registered file type that inherits from the IHttpHandler interface and will be interpreted at runtime (rather than being required to be registered in the web.config).

Configuring the HttpHandler

The template code from Visual Studio for an HttpHandler looks like:

<%@ WebHandler Language=”C#” Class=”SqlPhotoHandler” %>

using System;
using System.Web;

public class SqlPhotoHandler : IHttpHandler {

public void ProcessRequest (HttpContext context) {
context.Response.ContentType = “text/plain”;
context.Response.Write(“Hello World”);
}

public bool IsReusable {
get {
return false;
}
}
}

There is one quick modification to make for our handler—we must allow it to be reusable. Above, IsReusable must return a true value to allow our handler to be reused for fulfilling other requests of the same type.

So, what does this code do? Quite simply, it sets the ContentType to text and outputs (or writes to the page) “Hello World!”. That’s it. We’re going to change that!

Step #1 – Add the required namespaces. For this handler, we will need to access our web.config, connect to a SQL Server, and manipulate IO streams and images.

using System;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Web;

Step #2 – Define our variables. For this example, the handler must collect two pieces of information from the query string (the integer id of the photo and a Boolean on whether or not to output a thumbnail or the full photo), a byte array to hold the image information, and a string for the content type. Finally, we’ll capture the two query string values and place them in our variables. These will be placed right inside the ProcessRequest method.

int photo_id = 0;
bool thumbnail = false;
byte[] storedImage = null;
string contentType = “”;
photo_id = Convert.ToInt32(context.Request.QueryString[“photo_id”]);
thumbnail = Convert.ToBoolean(context.Request.QueryString[“thumbnail”]);

Step #3 – Connect to SQL and read our data. To do this, we’ll use a standard SqlDataReader. It is important to remember that our photo is binary data and must be explicitly converted to a byte array (byte[]) as it is streamed in.

if (photo_id != 0)

{

string sql = “SELECT photo, contenttype FROM photos WHERE photo_id = @photo_id”;

using (SqlConnection conn =
new SqlConnection(ConfigurationManager.ConnectionStrings[“PhotoStorage”].ConnectionString))
{

conn.Open();
using (SqlCommand cmd = new SqlCommand(sql, conn))
{

cmd.Parameters.Add(“@photo_id”, SqlDbType.Int).Value = photo_id;
using (SqlDataReader reader = cmd.ExecuteReader())
{

while (reader.Read())
{
storedImage = (byte[])reader[“photo”];
contentType = (String)reader[“contenttype”];
} } } }

Step #4 – Dance around the Image streams. Now, I’m sure the fine print that reads “You should avoid saving an image to the same stream that was used to construct it. Doing so might damage the stream” is worthwhile (located on the Image.Save method from MSDN), but it took me AGES to find it and find a definition of the error. What this really is saying is: doing so will cause the compiler to error out a non-sense error.

So what do we do to avoid this problem? We stream our image from our byte[] array into ANOTHER stream before we place it into an image.

Image imageFromStream;

using (MemoryStream stream = new MemoryStream(storedImage))
{
imageFromStream = Image.FromStream(stream);
}
Bitmap fullSizeImage = new Bitmap(imageFromStream);

So now we have a new Bitmap object to work with called fullSizeImage.

Step #4 – Is this a full-size image or thumbnail image request? Using the Boolean value we captured from the query string, now we need to determine what to output to the HTTP stream—the full-sized image from the database or a constructed .GetThumbnailImage as we created in Part #1. The difference here is rather than attaching a file system object, an image, to a hyperlink button, we’re simply returning an image (in the ImageFormat of Jpeg*) to the HTTP stream.

if (!thumbnail)

{

context.Response.ContentType = contentType;
fullSizeImage.Save(context.Response.OutputStream, ImageFormat.Jpeg);
ImageFromStream.Dispose();
fullSizeImage.Dispose();
context.Response.End();

}

else

{

// Determine height and width.

int tH = 120;
int tW = 120;
int oH = fullSizeImage.Height;
int oW = fullSizeImage.Width;

if (!(oH < 120 | oW < 120))
{
tH = oH / 3;
tW = oW / 3;
}

Image thumbnailImage = fullSizeImage.GetThumbnailImage(tW, tH, null, new IntPtr());
context.Response.ContentType = contentType;
thumbnailImage.Save(context.Response.OutputStream, ImageFormat.Jpeg);
fullSizeImage.Dispose();
imageFromStream.Dispose();
thumbnailImage.Dispose();

}

* In a production situation, you would take into consideration the contentType value stored in the database (and collected on by the FileUpload object) and stream the correct ImageFormat property out. In my experiences, Jpeg works for most file types while testing.

Test our HttpHandler and Database Connection

Now we’re ready to test both our HttpHandler and our database.

Build and run your project and then browse to ~\SqlPhotoHandler.ashx?photo_id=(a valid ID from your database, we’ll use 3 for this example.

photogallery_part2_test2.GIF

There we have a nice image uploaded to the SQL Database. Now, try out the “if thumbnail == true” logic by appending “&thumbnail=true” to your address.

photogallery_part2_test3.GIF

Success!

Things to Remember

One of the most important lessons about HttpHandlers is to remember that you have a GREAT deal of flexibility here—you could allow custom resizing of images by placing height and width variables in the query string, place watermarks on images on the fly, etc.

Coming in Part #3

In Part #3, we’ll examine how to use the HttpHandler dynamically by building GridViews with TemplateFields.

I’ve attached the updated PhotoStorage.cs and SqlPhotoHandler.ashx code as photostorage_part2_code.pdf to the blog for your reference.

Categories: .net 2.0, c#
  1. Lanier Hall
    February 13, 2007 at 7:51 am

    I’m creating the exact same thing and our code is eerily similar…one thing I have, though, is instead of a thumbnail variable in the query string I have a size variable. If the size is 0 that means dont change the size, but if it’s anything else, I want to change the size to that…I’m having mixed success with this method, some pictures resize nicely and others just look terrible. They’re the correct dimensions but it just looks like their resolutions have been drastically downgraded…

    anyway, anyone know why this might be happening? I’m using GetThumbnailImage
    well, let me know when part 3 is written
    -CLH

  2. February 13, 2007 at 8:12 am

    I think it has to do specifically with the type and layout of the image. Part #3 is almost done–other obligations have been keeping me from ‘fun’ coding lately.😦

  1. May 21, 2007 at 2:17 pm
Comments are closed.
%d bloggers like this: