![]() |
Web Development »
Charts, Graphs and Images »
Images and multimedia
Intermediate
License: The GNU General Public License (GPL)
Gallery Server Pro - An ASP.NET Gallery for Sharing Photos, Video, Audio and Other MediaBy Roger MartinGallery Server Pro is a complete, stable ASP.NET gallery for sharing photos, video, audio and other media. This article presents the overall architecture and major features. |
XML, CSS, SQL, HTML, XHTML, C# 2.0, VB 8.0.NET 2.0, Win2K, WinXP, Win2003, Vista, .NET 3.0, ASP.NET, SQL Server, GDI+, Ajax, VS2005, DBA, Dev
|
|
Advanced Search |
|
|
|
||||||||||||||||
Gallery Server Pro is a powerful and easy-to-use ASP.NET web application that lets you share and manage photos, video, audio, and other files over the web.
You can play with an online demo of Gallery Server Pro to get a sense of its capabilities. A pre-compiled version is available, including additional documentation and a support forum.
This project started in 2002 from my desire to share my photos over the web. I wanted my photos to remain on my own server, not somebody else's like Flicker or Shutterfly. Since there weren't any free solutions to choose from at the time, I wrote my own.
It turned out so well that I released version 1 to the world in January 2006, and it was downloaded over 30,000 times. I spent most of 2006 and 2007 working on version 2, rewriting the code from the ground up to use the new features of .NET 2.0. This includes ASP.NET Membership, Roles, Profiles, generics, the data provider model, and several well known design patterns (strategy, iterator, factory, template method, and composite). Version 2.0 was released in October 2007.
In September 2008 I released 2.1. It contains several new features and dozens of bug fixes. More details about the release can be found at www.galleryserverpro.com
In this article I present the overall architecture and major features of Gallery Server Pro. The topics presented here can help if you want to learn more about:
Gallery Server Pro is a fully functional and stable web application ready for production use.
Gallery Server Pro stores media objects such as photos, video, audio, and documents in albums. These files and albums are stored in a directory named mediaobjects within the web application. (This can be changed to any location on the web server.) An album is really just a directory, so an album named Vacation Photos is stored as a similarly named directory.
There are two main techniques for adding media objects:
When adding a media object, the following steps occur:
Media objects are streamed to the browser through an HTTP handler. Below you can see a photo and a video being displayed. If watermarking is enabled, the watermark is applied to the in-memory version of the image just before it is sent.
|
|
If you click the View metadata toolbar item above a media object, a popup DIV window displays the image's metadata, as shown below.
By default, everyone can browse the media objects. However, you must log on to perform any action that modifies an album or media object. Authorization to modify data is configured by type of permission and the albums to which it applies. For example, you can set up user Soren to have edit permission to the album Soren's photos. Another user Margaret is given edit permission to the album Margaret's photos. Each user can administer his or her own album but cannot edit albums outside his or her domain.
To learn more about how to use Gallery Server Pro from an end-user perspective, read the Administrator's Guide. Otherwise, read on to learn about the architecture and programming techniques.
The Visual Studio solution in the download contains ten projects. They are:
| Project name | Description |
gsweb |
UI layer - ASP.NET 2.0 web application |
TIS.GSP.WebControls |
Contains custom web server controls used in the web app |
TIS.GSP.Business |
Business layer logic |
TIS.GSP.Provider |
Data provider. Defines the contract the data layer must comply with |
TIS.GSP.Business.Interfaces |
Defines all interfaces used in the solution |
TIS.GSP.ErrorHandler |
Provides error handling support |
TIS.GSP.Configuration |
Provides read/write access to settings stored in the configuration file galleryserverpro.config |
TIS.GSP.Business.Wpf |
Provides enhanced image metadata extraction through the use of new WPF classes available in .NET 3.0. Contained in a separate project and invoked through reflection in a way that degrades gracefully when .NET 3.0 is not present |
TIS.GSP.Data.SQLite |
SQLite data layer logic. Provides read/write access to data stored in SQLite |
TIS.GSP.Data.SqlServer |
SQL Server data layer logic. Provides read/write access to data stored in SQL Server |
TracingTools |
Provides tracing support |
User accounts are managed through the ASP.NET Membership, Roles, and Profile APIs. By default, Gallery Server Pro is configured to use a locally stored SQLite database named galleryserver_data.sqlite in the App_Data directory. It interacts with this database by using SQLiteMembershipProvider for users, SQLiteRoleProviderfor roles, and SQLiteProfileProvider for profiles.
However, because of the flexibility offered by the provider model, you can use any data store that has a membership provider. For example, you can use SqlMembershipProvider to use SQL Server or ActiveDirectoryMembershipProvider to plug Gallery Server Pro into your existing base of Active Directory users. The Administrator's Guide contains a section for Membership Configuration that provides more information.
Recall that each media object (photo, video, etc) is stored in an album. Albums can be nested within other albums, with no restriction on the number of levels. This is similar to how files and directories are stored on a hard drive.
It turns out that albums and media objects have a lot in common. They both have properties such as Id, Title, DateAdded, and FullPhysicalPath; and they both have methods such as Save, Delete, Copy, Remove, and ValidateTitle. This is the ideal situation in which to use the "Composite" design pattern, where common functionality is defined in a base object. I start by defining two interfaces — IGalleryObject and IAlbum:
The IAlbum interface inherits from IGalleryObject and then adds a method and a few properties that are specific to albums. Then I create the abstract base class GalleryObject. It implements the IGalleryObject interface and provides default behavior that is common to albums and media objects. For example, here is the Title property:
public string Title
{
get
{
VerifyObjectIsInflated(this._title);
return this._title;
}
set
{
value = ValidateTitle(value);
this._hasChanges = (this._title == value ? _hasChanges : true);
this._title = value;
}
}
Now that the common functionality is defined in the abstract base class, I can create concrete classes to represent albums, images, video, audio, and other types of media objects:
With this approach there is very little duplicate code, the structure is maintainable, and it is easy to work with. For example, when Gallery Server Pro wants to display the title and thumbnail image for all the objects in an album, there might be any combination of child albums, images, video, audio, and other documents. But I don't need to worry about all the different classes or about casting problems. All I need is the following code:
// Assume we are loading an album with ID=42
IAlbum album = Factory.LoadAlbumInstance(42, true);
foreach (IGalleryObject galleryObject in album.GetChildGalleryObjects())
{
string title = galleryObject.Title;
string thumbnailPath = galleryObject.Thumbnail.FileNamePhysicalPath;
}
Beautiful, isn't it? But what happens when the functionality is slightly different between two types of objects? For example, Gallery Server Pro needs to enforce a maximum length of 200 characters for an album title and 1000 characters for the title of a media object (image, video, etc). Both types of objects need a Title property, but the validation is different. Does that mean we have to remove the Title property from the base class and put it in the derived classes?
Not at all! Refer back to the property definition for Title in the code we looked at earlier. Notice that in the setter there is a call to ValidateTitle. Here is what ValidateTitle looks like in the GalleryObject class:
protected virtual string ValidateTitle(string title)
{
// Validate that the title is less than the maximum limit.
// Truncate it if necessary.
int maxLength =
GalleryServerPro.Configuration.ConfigManager.
GetGalleryServerProConfigSection().DataStore.MediaObjectTitleLength;
if (title.Length > maxLength)
{
title = title.Substring(0, maxLength).Trim();
}
return title;
}
The procedure is defined as virtual, allowing a derived class to override it if needed. In fact, that is exactly what the Album class does:
protected override string ValidateTitle(string title)
{
int maxLength =
GalleryServerPro.Configuration.ConfigManager.
GetGalleryServerProConfigSection().DataStore.AlbumTitleLength;
if (title.Length > maxLength)
{
title = title.Substring(0, maxLength).Trim();
}
return title;
}
The end result is that there is a base implementation in the base class that provides functionality for most cases, and code that is unique to albums is contained in the Album class. There isn't any duplicate code and the logic is nicely encapsulated. It is a thing of beauty to behold.
We just saw how to override a method in the base class when we need to alter its behavior. I could have done something similar when it comes to saving the albums and media objects to the database. The Save method in the GalleryObject class could have been defined as virtual, and I could have overridden the method in each of the derived classes. But since the classes Image, Video, Audio, GenericMediaObject, and ExternalMediaObject all represent objects that get stored in the same table (gs_MediaObject), that would have meant writing the same code in all four classes, with only the Album class being different.
I could eliminate the problem of duplicate code by providing a default implementation in the Save method in the GalleryObject class. In that method, I save to the media object table, and then depend on the Album class to override the behavior, much like we did with the ValidateTitle method. However, this is putting a substantial amount of behavior in a base class that doesn't really belong there. We should limit the base class to contain state and behavior that applies to ALL derived objects.
You might argue that I violated this rule when I provided a default implementation of the ValidateTitle method that I overrode in the Album class. You are absolutely right. But I justify it by suggesting that implementing the title validation in every derived class creates undesirable duplicate code, and refactoring it to use the strategy pattern is overkill. These are not hard and fast rules. Architecting an application is as much art as it is science, and you must weigh the pros and cons of each approach.
Getting back to our challenge of persisting data to the data store, the approach I came up with was to use the strategy pattern to encapsulate behavior. First, I defined an interface ISaveBehavior :
public interface ISaveBehavior { void Save(); }
Then I wrote two classes that implemented the interface: AlbumSaveBehavior and MediaObjectSaveBehavior. The Save method takes care of persisting the object to the hard drive and data store. For example, here is the Save method in AlbumSaveBehavior:
public void Save()
{
if (this._albumObject.IsVirtualAlbum)
return; // Don't save virtual albums.
// Save to disk.
PersistToFileSystemStore(this._albumObject);
// Save to the data store.
GalleryServerPro.Provider.DataProviderManager.Provider.Album_Save
(this._albumObject);
}
Notice that there is a call to PersistToFileSystemStore, which is a private method that ensures a directory exists corresponding to this album. Then there is a call to the Album_Save method of the Provider class, which persists the data to the gs_Album table in SQLite. If you use a data provider other than the default SQLiteMembershipProvider, then the method delegates to that provider. We'll talk more about the data provider model later in this article.
OK, we have two classes for saving data to the data store — one for albums and one for media objects. How do we invoke the appropriate Save method from the GalleryObject base class?
Recall that the GalleryObject class is abstract, so it can never be directly instantiated. Instead, we instantiate an instance of the Album, Image, Video, Audio, GenericMediaObject, or ExternalMediaObject class. The constructor for each of these classes assigns the appropriate save behavior. For example, in the constructor of the Album class, we have:
this.SaveBehavior = Factory.GetAlbumSaveBehavior(this);
The GetAlbumSaveBehavior method just returns an instance of the AlbumSaveBehavior class:
public static ISaveBehavior GetAlbumSaveBehavior(IAlbum albumObject)
{
return new AlbumSaveBehavior(albumObject);
}
The SaveBehavior property of the GalleryObject class is of type ISaveBehavior. Since both classes implement this interface, we can assign instances of either class to the property.
The Save method in the GalleryObject class simply calls the Save method on the SaveBehavior property. It has no idea whether the property is an instance of AlbumSaveBehavior or MediaObjectSaveBehavior, and it doesn't care. All that matters is that each class knows how to save its designated object.
This is an example of using the strategy pattern. Specifically, the strategy pattern is defined as a family of algorithms that are encapsulated and interchangeable. In our case, we have two save behaviors that are self-contained and can both be assigned to the same property (interchangeable). It is a powerful pattern and has many uses.
Displaying an image in a web browser is easy because all browsers recognize the <img> tag. But how does one implement a gallery that also includes video, audio, and documents such as Adobe PDF or Microsoft Word files? Browsers, by themselves, cannot play video and audio or natively display any files other than a few basic types like HTML, TXT, and maybe XML. These types of objects require plug-ins, and one cannot make any assumptions about what, if any, plug-ins are installed in a user's browser. Furthermore, Gallery Server Pro is meant to be flexible so that administrators can customize the HTML output rendered by Gallery Server Pro. For example, some users might prefer that video files are rendered with <object> tags in order to pass XHTML validation, while others might prefer <embed> for maximum backward compatibility. Lastly, different browsers require different syntax, and new versions of browsers are frequently released, potentially breaking something that works today.
The solution was to use a combination of HTML templates stored in a configuration file and automatic browser sniffing provided by ASP.NET. The configuration file galleryserverpro.config (stored in the config directory) contains HTML templates for each major type of media object. For example, rendering the HTML for an image is pretty straightforward. Here is the relevant entry in galleryserverpro.config:
<galleryObject>
<mediaObjects>
<mediaObject mimeType="image/*">
<browsers>
<browser id="default" htmlOutput="<div class="op1">
<div class="op2"><div class="sb">
<div class="ib"><img id="mo_img"
src="{MediaObjectUrl}" class="{CssClass}"
alt="{TitleNoHtml}" title="{TitleNoHtml}"
style="height:{Height}px;width:{Width}px;" />
</div></div></div></div>" />
The mediaObject tag specifies that the inner tags apply to the image/* MIME type. The asterisk (*) means that it will match all images, such as image/jpg, image/jpeg, and image/gif. Now, let's take a closer look at what is in the htmlOutput attribute of the browser tag:
<div class="op1">
<div class="op2">
<div class="sb">
<div class="ib">
<img id="mo_img" src="{MediaObjectUrl}"
class="{CssClass}" alt="{TitleNoHtml}"
title="{TitleNoHtml}" style="height:{Height}px;width:{Width}px;" />
</div>
</div>
</div>
</div>
Note: In the configuration file the <, >, and " characters within the htmlOutput attribute are escaped with <, > and ", but I show them here for easier readability.
The four <div> tags that surround the <img> tag support the drop shadow and border that appear around each image. But what is that stuff in the brackets, like {MediaObjectUrl}? Those are placeholders that are replaced with dynamically generated content at runtime. For example, {Height} is replaced with the height of the image. A full list and descriptions of these placeholders can be found in the Administrator's Guide.
When Gallery Server Pro uses the above template to render an image to the browser, it ends up looking something like this:
<div class="op1">
<div class="op2">
<div class="sb">
<div class="ib">
<img id="mo_img"
src="http://www.codeproject.com/gs/handler/getmediaobject.ashx?
moid=2064&aid=169&mo=D%3A%5Cgs%5Cmediaobjects%
5CzThumb_100_1768.jpeg&mtc=1&dt=1" alt="Grand Canyon"
style="width:86px;height:115px;" />
</div>
</div>
</div>
</div>
If desired, one can tweak this template to change how <img> tags are rendered. For example, if you wanted to use the width and height attributes instead of a style, update the config file like this:
<browser id="default"
htmlOutput="<div class="op1"><div class="op2">
<div class="sb"><div class="ib">
<img id="mo_img" src="{MediaObjectUrl}"
class="{CssClass}" alt="{TitleNoHtml}"
title="{TitleNoHtml}" width="{Width}" height="{Height}" />
</div></div></div></div>" />
Image rendering is pretty straight forward, and the technique presented here would be overkill if all we were doing was displaying images. But it becomes worthwhile once we get into the more complicated media types. For example, let's look at the relevant section of galleryserverpro.config that describes how video is rendered:
<mediaObject mimeType="video/*">
<browsers>
<browser id="default"
htmlOutput="<object type="{MimeType}"
data="{MediaObjectUrl}" style="width:{Width}px;height:{Height}px;" >
<param name="src" value="{MediaObjectUrl}" />
<param name="autostart" value="{AutoStartMediaObjectInt}" />
</object>" />
Notice there are two mediaObject elements - one that specifies the MIME type video/* and the other that specifies video/quicktime. The HTML template under video/* handles all generic cases of rendering video. The second element handles the more specific case of QuickTime video, since that type requires different HTML. One can add additional mediaObject elements if, for example, AVI files required custom HTML. In fact, the config file does contain specific templates for avi, wmv, asf, and asx files.
Take a closer look at the browser entries inside the video/* template. There are two browser elements here - one with id="default" and the other id="ie". The id attribute uniquely identifies the browser element, and it must match the internal identifier of a browser as specified in the .NET Framework's browser definition file. You can find a hierarchical list of ids recognized by .NET Framework 2.0 on the internet.
These two elements specify one HTML template for the default browser, and another for IE. If you look at the HTML templates closely, you will notice they are identical except that one specifies AutoStartMediaObjectInt and the other AutoStartMediaObjectText. That's because IE requires the autostart parameter to be text, like true or false, while other browsers require a numerical value (1 for true, 0 for false).
If, for example, you discovered that the Safari browser wasn't working quite right, you could add a browser element with id="safari". Then add the HTML template using the syntax required by Safari, and you are finished. Gallery Server Pro and ASP.NET automatically detect the browser type and send the correct HTML.
One of the cool new features of ASP.NET 2.0 is the "provider model." In Gallery Server Pro, I used the provider model to define the API for reading and writing data to the data store. This allows one to use any source for data storage as long as a provider is written for it. Gallery Server Pro 2.1 contains two providers - SQL Server and SQLite. Additional providers can be written that use MySQL, Oracle, Microsoft Access, or even an XML file as the data store.
The diagram below shows the SQLiteGalleryServerProProvider and DataProvider classes. The DataProvider class is an abstract class that inherits from the Microsoft .NET Framework class System.Configuration.Provider.ProviderBase. It doesn't contain any behavior; it only defines the methods that must be implemented by the "real" data provider, which in this case is SQLiteGalleryServerProProvider. All data access in Gallery Server Pro passes through one of the methods in SQLiteGalleryServerProProvider (except user account functions that pass through the other providers — membership, roles, and profile).
To use an alternative data store such as MySQL, Oracle, Microsoft Access, or something else, write a new class that inherits from the DataProvider abstract base class. This is best done in a new class library project. If you use a tool such as Visual Studio, it will automatically define all the methods that must be implemented. For example, here is the skeleton for the method to delete an album:
public override void Album_Delete(IAlbum album)
{
}
Gallery Server Pro will call this method whenever an album is to be deleted. It is your job, as the writer of this custom provider, to write the code that will delete the album record from your data store. How you do it is up to you.
Note: Refer to the code in the SQLiteGalleryServerProProvider class to ensure you provide similar behavior. For example, when deleting an album the SQLiteGalleryServerProProvider class executes a stored procedure that recursively deletes all child albums of the specified album. Your custom provider should behave similarly.
Once you have implemented all the methods and compiled your code, you are ready to configure Gallery Server Pro to use your provider. Copy the DLL containing your provider into the bin directory of the Gallery Server Pro web application. Update the data provider section of galleryserverpro.config. For example, if your provider is in a class named OracleDataProvider that is in an assembly named GalleryServerPro.Data.Oracle.dll, the data provider section might look like this:
<dataProvider defaultProvider="OracleDataProvider">
<providers>
<add name="OracleDataProvider"
type="GalleryServerPro.Data.Oracle.OracleDataProvider,
GalleryServerPro.Data.Oracle" />
</providers>
</dataProvider>
Image files, most commonly JPGs, can contain metadata such as camera model and shutter speed. In addition, utilities such as Vista's Photo Gallery allow users to add keywords, titles, ratings, and more. Gallery Server Pro can extract this data in any of the following formats: EXIF, XMP, tEXt, IFD, and IPTC.
The code to extract metadata is based on the Code Project article A Library to Simplify Access to Image Metadata, which itself was based on the article Photo Properties. I'd like to thank these authors for their hard work. The techniques in these articles are based on parsing the metadata that is accessible through the PropertyItems property of a System.Drawing.Image object. I refactored much of the code to make it easier to understand (and therefore maintain), faster, more flexible, and more robust.
The introduction of the System.Windows.Media.Imaging namespace in .NET 3.0 provided an improved method of extracting metadata, including the ability to get data not previously accessible — most notably titles and keywords.
So now there are two ways to get metadata from an image — the .NET 2.0 way and the .NET 3.0 way. While the .NET 3.0 technique is better, I wanted Gallery Server Pro to work on a system without .NET 3.0 installed. As a result, the metadata is extracted using the following process:
BitmapMetadata class in the System.Windows.Media.Imaging namespace to extract as much metadata as possible into a custom collection named GalleryObjectMetadataItemCollection. System.Imaging.Image object. Use the PropertyItems property to extract as much metadata as possible. For each metadata item (e.g. shutter speed), add it to the GalleryObjectMetadataItemCollection collection, but only if it wasn't already added in step 1. In other words, if the same metadata item is extracted using both techniques, we keep the data from the .NET 3.0 method and discard the .NET 2.0 version.
The logic for extracting metadata is hidden behind the business layer class MediaObjectMetadataExtractor. When an image is added to the gallery, the following code is executed in the GalleryServerPro.Business.Image constructor:
Metadata.MediaObjectMetadataExtractor metadata =
new Metadata.MediaObjectMetadataExtractor(imageFile.FullName);
this.MetadataItems.AddRange(metadata.GetGalleryObjectMetadataItemCollection());
The variable imageFile is an instance of System.IO.FileInfo that refers to the image. The MetadataItems property is a GalleryObjectMetadataItemCollection collection. Once the metadata is extracted and saved, one can easily iterate through the items and get the name/value pairs:
// Assume we are loading an image with ID=27
IGalleryObject image =
GalleryServerPro.Business.Factory.LoadMediaObjectInstance(27);
foreach (IGalleryObjectMetadataItem metadataItem in image.MetadataItems)
{
string name = metadataItem.Description; // e.g. Camera model, Shutter speed
string value = metadataItem.Value; // e.g. F5.7, 1/350 sec
}
This has been a brief introduction to the architecture and programming techniques used in Gallery Server Pro. Feel free to download the source code and use the bits to help in your own project. Cheers!
2008 Nov 3: Updated source code to 2.1.3222.
2008 Sep 9: Updated to include latest source files, references to new features, and minor content updates.
2008 May 6: Updated to include latest source files and minor content updates.
2007 Oct 28: Article release.
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 6 Nov 2008 Editor: Sean Ewington |
Copyright 2007 by Roger Martin Everything else Copyright © CodeProject, 1999-2009 Web11 | Advertise on the Code Project |