Tuesday, November 8, 2011
Video Gallery in Asp.Net
Introduction
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. The entire application is contained in a single ASCX user control for easy insertion into your own web sites.
- Stable, production ready
- Scalable to hundreds of thousands of objects
- Use any web browser to organize your media files into albums you can easily add, edit, delete, rotate, rearrange, copy and move
- Easily add files using one-click synchronize and ZIP file upload functions. Thumbnail and compressed versions are automatically created
- Add links to external media objects such as YouTube videos
- Powerful user security with flexible, per-album granularity
- Available as a native DotNetNuke module
- Metadata extraction
- Search by title, caption, filename, and metadata
- Image watermarking with your own text and/or image
- AJAX-enabled for more responsive UI
- Web-based installer makes installation painless
- Your choice of database: Zero-configuration, mostly ACID-compliant SQLite database or Microsoft SQL Server for mission critical applications
- Silverlight and Flash used to play compatible video and audio files
- Uses ASP.NET Membership provider so you can integrate with your existing accounts, including Active Directory
- Data access uses the ASP.NET Provider model, which allows other data stores such as MySQL, Microsoft Access, or Oracle to be used in addition to SQL Server
- 100% managed code written in C# and ASP.NET 2.0
- Source code is released under the open source GNU General Public License
- All web pages target XHTML 1.0 Strict and CSS 2.1 standards to ensure maximum forward compatibility
Background
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.
Version 1 was released to the world in January 2006. Since that time, there have been hundreds of thousands of downloads and a steady stream of releases. At the time of this writing, the latest version is 2.4.7. Checkgalleryserverpro.com for the latest version.
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:
- Implementing a web gallery to share photos, video, audio, and other documents
- Using ASP.NET Membership, Roles and Profile API
- Using the composite design pattern to manage infinitely hierarchical relationships. In this case, it is media objects and albums, but it also applies to employee/supervisor relationships, bill of materials, file/directory relationships, and other similar structured items.
- When and how to use the strategy design pattern
- Using the data provider model in ASP.NET
- A flexible technique for rendering distinct HTML to a browser based on the type of browser and type of object being rendered
- Extracting metadata from images using the .NET 2.0 technique and the new WPF classes in .NET 3.0+
Using Gallery Server Pro
Gallery Server Pro is a fully functional and stable web application ready for production use.
- Download the source code in this article and compile. Or download the compiled version.
- Use Internet Information Services (IIS) Manager to configure the directory as a web application.
- Use Windows Explorer to give the IIS application pool modify permission to the web application directory.
- Use a web browser to start the application. The install wizard will automatically start.
- Step through the install wizard. When finished, you have a working version of Gallery Server Pro.
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 gs\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:
- Upload a ZIP file containing the media files. If the ZIP file contains directories, they are converted to albums.
- Copy your media files to the media objects directory, and then start a synchronization in Gallery Server Pro.
When adding a media object, the following steps occur:
- The file is saved to the media objects directory. (If adding a media object via the synchronization technique, then this step is already done.)
- A thumbnail image is created and saved to the hard drive.
- For images, a compressed, bandwidth-friendly version is created and metadata such as camera model and shutter speed is extracted.
- A record is added to the data store to represent this media object.
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 userMargaret 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.
Solution Architecture
The Visual Studio solution in the download contains twelve projects. They are:
Project name | Description |
Website | 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.Business.Resources | Contains resource data to support the business layer |
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 custom configuration settings in web.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.Business.Wpf.Resources | Contains resource data to support the WPF project |
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 |
User Management and Security
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, SQLiteRoleProvider
for 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 orActiveDirectoryMembershipProvider
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.Media Object, Albums and the Composite Pattern
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
, andValidateTitle
. 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.Using the Strategy Pattern for Persisting to the Data Store
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 theGalleryObject
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
, andExternalMediaObject
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 theGalleryObject
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 thestrategy 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 defaultSQLiteMembershipProvider
, 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 theAlbum
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.
Rendering HTML for Video, Images, Audio and More
Browsers, by themselves, typically cannot play video, audio, or render many kinds of documents like Adobe PDF or Microsoft Word files. These types of objects require plug-ins which may or may not be installed in your users' browsers.
Gallery Server Pro is flexible so that administrators can customize the HTML output rendered by Gallery Server. You, as the web site administrator, may feel comfortable having a dependence on a particular plug-in, such as Silverlight or Flash, while others may not. Also, some users might prefer that multimedia 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.Gallery Server Pro uses a combination of automatic browser sniffing and HTML and script templates stored in the database. The table
gs_BrowserTemplate
contains templates for each type of media object. For example, rendering the HTML for an image is pretty straightforward:<div class="gsp_i_c" style="width:{Width}px;">
<img id="mo_img" src="{MediaObjectUrl}" class="{CssClass}" alt="{TitleNoHtml}"
title="{TitleNoHtml}" style=" width:{Width}px;height:{Height}px;" />
</div>
The text in brackets – like {
MediaObjectUrl
} – 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 below.When Gallery Server Pro uses the above template to render an image to the browser, it ends up looking something like this:
Collapse | Copy Code
<div class="gsp_i_c" style="width:86px;">
<img id="mo_img"
src=http://www.codeproject.com/gs/handler/getmediaobject.ashx?moid=5&dt=2&g=1
class=""
alt="Grand Canyon" title="Grand Canyon" style="width:86px;height:115px;" />
</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 template to this:<div class="gsp_i_c" style="width:{Width}px;">
<img id="mo_img" src="{MediaObjectUrl}" class="{CssClass}" alt="{TitleNoHtml}"
title="{TitleNoHtml}" height="{Width}" width="{Height}"> />
Rendering Browser-specific HTML
Let us jump in with a real world example of how Gallery Server Pro sends one browser different HTML than other browsers for a particular media type – in this case, images.
Gallery Server Pro renders images in a white border with rounded corners and a drop shadow. CSS is used to provide the rounded corner and drop shadow effect, but Internet Explorer 8 and earlier does not support these CSS effects. For these versions, we use a different drop shadow technique that requires a more complicated HTML structure:
<div class="gsp_floatcontainer">
<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>
</div>
In other words, when displaying images in the browser, we need one HTML pattern for IE 1-8 and another HTML pattern for all other browsers, including IE 9. We achieve this by having two rows in the
gs_BrowserTemplate
table – one to handle the default rendering for images and another for IE 1-8 users:Gallery Server Pro automatically matches users with Internet Explorer 1-8 and matches them to the template with the browser ID "ie1to8". Everyone else gets the default template.
Browse the remaining records in this table to see how Gallery Server Pro handles other media types. There is a
ScriptTemplate
column that can contain JavaScript to execute for each media object. This is an important column for Flash and Silverlight rendering. Feel free to modify the templates to suit your requirements. Note that Gallery Server Pro caches these records, so recycle the application pool after any changes to the table.Browser ID Reference
Below are two lists of valid browser IDs – one for .NET 2.0 – 3.5 and the other for .NET 4.0.
The browser IDs are hierarchical, which means you can use one ID to match a subset of browsers. For example, entering the browser ID "Opera" will match all versions of Opera, while entering "Opera8to9" will match only Opera 8-9.
Notice that in .NET 4, there are new browser IDs for BlackBerry, iPhone, iPod, and IEMobile, allowing one to send targeted HTML to those systems. The current version of Gallery Server Pro does not take advantage of this, but you can by adding rows to
gs_BrowserTemplate
.Side Note: Identifying Internet Explorer 1-8
You may have noticed the browser definitions shown above don't contain "IE1to8", which is what we used in our example. By default, Microsoft doesn't provide any way to target IE 1 to 8, so Gallery Server Pro does a little extra processing to check for this condition so we can use it as a valid browser ID.
Data Provider Model
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 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. TheDataProvider
class is an abstract
class that inherits from the Microsoft .NET Framework classSystem.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
: Collapse | Copy Code
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 album
s 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 in the
<galleryServerPro>
section of web.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:<galleryServerPro>
<core galleryResourcesPath="gs"/>
<dataProvider defaultProvider="OracleDataProvider">
<providers>
<clear/>
<add name="OracleDataProvider"
type="GalleryServerPro.Data.Oracle.OracleDataProvider,GalleryServerPro.Data.Oracle"
applicationName="Gallery Server Pro" connectionStringName="OracleDbConnection" />
</providers>
</dataProvider>
</galleryServerPro>
Image Metadata Extraction
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:
- If .NET 3.0 is installed on the web server, use the
BitmapMetadata
class in theSystem.Windows.Media.Imaging
namespace to extract as much metadata as possible into a custom collection namedGalleryObjectMetadataItemCollection
. - Instantiate the image into a
System.Imaging.Image
object. Use thePropertyItems
property to extract as much metadata as possible. For each metadata item (e.g. shutter speed), add it to theGalleryObjectMetadataItemCollection
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 theGalleryServerPro.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
}