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. 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
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.).
If you click the View metadata toolbar item above a media object, a popup DIV window displays the image's metadata, as shown below. Visual Studio solution in the download contains ten projects. They are:
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,.
Recall that,
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 = 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 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 I came up with was to use the strategy pattern to encapsulate behavior. First, I defined an interface:
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.:
this.SaveBehavior = Factory.GetAlbumSaveBehavior(this);
The
GetAlbumSaveBehavior method just returns an instance of the
AlbumSaveBehavior class:.
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=">" />
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:
>
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="? 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:
BitmapMetadataclass in the
System.Windows.Media.Imagingnamespace to extract as much metadata as possible into a custom collection named
GalleryObjectMetadataItemCollection.
System.Imaging.Imageobject. Use the
PropertyItemsproperty to extract as much metadata as possible. For each metadata item (e.g. shutter speed), add it to the
GalleryObjectMetadataItemCollectioncollection, 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