digital expertise network :: about the guild :: case studies :: contact the guild :: articles :: second life :: tools

Dealing with images in content management systems (page 2)

Navigation: Page 1 | Page 2 | Page 3 | Page 4 | Page 5 | Samples

(Download the source code for this article)

So how does it all work?

There are eight source files:

WebImageMaker.cs
the source code for the server control class, WebImageMaker. Also contains definitions of three enumerations – ControlMode, which keeps track of what the control is currently doing (e.g., displaying the canvas), and WebImageFormat and WebImageQuality, which are also attributes of the control and allow the developer to enforce how the final web image is created.

IImageProvider.cs
An interface that defines the operations the control needs to perform on images. By encapsulating this, we could switch to a different image library in future. Most of the file I/O is done in here as well, because drawing APIs typically have the ability to read and write images to and from disk, and it might be unnecessarily awkward to separate out I/O operations from drawing operations.

ImageProviderImpl.cs
An implementation of IImageProvider that uses the GDI+ libraries in System.Drawing.

WebImageMakerImageHelper.cs
A helper class that's used to serve the images the control creates. This is separated off to optionally allow the control's generated images to be served by a different handler.

WebImageMakerHandler.ashx
Optional HttpHandler to use if you don't want the control itself to handle the serving of its generated images.

WebImageMaker.css
Client side style sheet that sets the CSS attributes for the various elements rendered by the control

WebImageMaker_canvas.js
Client-side script that powers the user interface when the control is in Canvas mode.

WebImageMaker_normal.js
Client-side script that helps display thumbnails for previously uploaded raw image files, when the control is not in Canvas mode.

Embedded Resources

These last three files are script and css resources used by the control. In ASP.NET 2.0 we can embed these resources into our assembly so that the control can be deployed in some other solution as a single dll with no dependent files. Requests for these files from the browser are directed at the assembly dll itself via the .axd handler. However, users of Visual Web Developer 2005 Express don't get the project option of building a control library that makes this easy in an IDE. For that you would normally need Visual Studio 2005. It is possible to develop an ASP.NET server control in Web Developer Express and then, with a little help from the compiler on the command line, build it as a control library complete with embedded resources. In development, we can use the files directly as part of the project. When building as a control, we can use them as embedded resources:

#if BuildAsControlLibrary cssUrl = Page.ClientScript.GetWebResourceUrl(this.GetType(), "Guild.WebControls.WebImageMaker.css"); #else cssUrl = Page.ResolveUrl("~/WebImageMaker.css"); #endif

"BuildAsControlLibrary" is a conditional compilation directive that we don't define anywhere in our project, so the #else clause will always be used when running from Web Developer Express. In WebImageMaker.cs we also conditionally compile three web resource attributes to register the files as part of the assembly and define the mime types that they should be served as:

#if BuildAsControlLibrary [assembly: WebResource("Guild.WebControls.WebImageMaker.css", "text/css")] [assembly: WebResource("Guild.WebControls.WebImageMaker_canvas.js", "application/x-javascript")] [assembly: WebResource("Guild.WebControls.WebImageMaker_normal.js", "application/x-javascript")] #endif

When running the project normally from Web Developer Express, the files will be served from the file system just like any other files. When you want to compile the project into a dll, you can use the supplied Build_Control_Library.bat file, which just contains the line:


csc @Build_Switches.rsp App_Code\*.cs

where Build_Switches.rsp is a file that contains the required compiler switches:


# define our conditional compilation label
/define:BuildAsControlLibrary

# embed the resources
/resource:WebImageMaker.css,Guild.WebControls.WebImageMaker.css 
/resource:WebImageMaker_canvas.js,Guild.WebControls.WebImageMaker_canvas.js 
/resource:WebImageMaker_normal.js,Guild.WebControls.WebImageMaker_normal.js 

# output as a library into our BuildOutput directory
/target:library /out:BuildOutput\Guild.WebControls.WebImageMaker.dll

Note the definition of BuildAsControlLibrary and the embedding of the three resources. The output in the BuildOutput directory can be copied into other web projects without affecting the original project.

You'll need a command prompt with the right environment variables. The easiest way of doing this is to use the Visual Studio Command Prompt if you have it; if you have Web Developer Express you can to use the SDK Command prompt (available from the "Microsoft .NET Framework SDK v2.0" program group on the start menu). I don't think the Express edition installs the SDK – it's worth getting.

Storage of uploaded files

The control uses the file system to store the uploaded files as well as the generated canvases, thumbnails and web images. It would be possible to use MemoryStreams stored in the user's Session, and avoid the need to have a writable directory for ASP.NET to store the images in, but this might get out of hand very quickly if more than a few people are using the system at any one time. By saving the images to the file system between postbacks and disposing of any memory hungry resources as soon as possible we make the system more scalable. Saving the images to disk also allows for an audit trail so we can see what types of images the users are uploading. The downside of this is that the image directories (especially the raw image directory) can fill up very quickly, so the control exposes a delegate that allows the developer to supply a purging strategy in the form of a method that the control will call before it writes to a directory. A default purging strategy is used if no others are provided.

public delegate void PurgeMethod(string directoryToClean);

Serving of generated images

Apart from the initial image the control is set to, all images generated by the control need to be served somehow. One way of doing this is to use an http handler in the form of an ashx file. The control has an optional HandlerPath property which should be set to point at the file. This file is provided as part of the project. However, it breaks the "no dependency" deployment scenario because it's an additional file and it also requires an entry in Web.Config to tell it where the working directory is. An alternative to this approach is to have the control itself responsible for serving its generated images. In the absence of a HandlerPath property the control will write out the URLs of generated images to point back to the control's containing page with parameters on the querystring that the control can read and optionally hijack the page request to serve the required image back out. This is done as early as possible to prevent any unnecessary work being done on the server:

protected override void OnInit(EventArgs e) { if (Page.Request.QueryString["mode" + KeySuffix] != null) { ... // get the details of the image from the querystring ... // write the image out to the response ... Response.End(); } ... }

In fact the code that reads the query string and writes the file out is separated out into the WebImageMakerImageHelper class and is used both by the control and the supplied handler (WebImageMakerHandler.ashx).

The control has a private property (IsServingImage) that it maintains to ensure that no unnecessary work is done before OnInit is called. This can be seen being checked in several places in the code.

The pros and cons of having a control hijacking its containing page's request in this way are discussed in a post to Fritz Onion's blog here:

http://pluralsight.com/blogs/fritz/archive/2005/02/11/5789.aspx

While having the control generate its own images is neat, there are a number if problems with it as can be seen from the discussion relating to the above post, not least the potentially unknowable amount of other work that ASP.NET might be doing on a request for the page and other controls before it gets round to calling the OnInit method of your particular control, where you can terminate the request early. This work might be decidedly non-trivial in a number of circumstances. On the whole I'd use the separate handler wherever possible. The sample page provided in the project uses both approaches in different instances of the control.

Child Controls

The control derives from the new ASP.NET 2.0 CompositeControl abstract class. This takes care of some of the things that had to be done by hand in v1.1 and also allows the control to be rendered in the designer without too much extra work.

The control's children are all straightforward Web Controls and Html controls with a few literals. Some of the properties of the WebImageMaker control map onto properties of child controls:

[Bindable(false)] [Category("Appearance")] [DefaultValue("Confirm Selection")] [Themeable(false)] public string ConfirmButtonText { get { EnsureChildControls(); return confirmSelection.Text; } set { if (!IsServingImage) // child controls won't be instantiated { EnsureChildControls(); confirmSelection.Text = value; } } }

Here, confirmSelection is a button that forms part of the Canvas UI. The call to EnsureChildControls() results in ASP.NET calling the control's CreateChildControls() method, which creates the control hierarchy.

CreateChildControls()

Note that much of this code has been stripped out – see the supplied source for the full picture.

protected override void CreateChildControls() { ... targetImage = new HtmlImage(); popupDiv = new HtmlGenericControl("div"); upload = new FileUpload(); // etc... and the rest of the controls ... targetImage.ID = this.ID + "_img"; popupDiv.Attributes.Add("class", "webImageMaker_popup"); // etc... add attributes to controls that need them for css and script ... this.Controls.Add(popupDiv); this.Controls.Add(hiddenField); popupDiv.Controls.Add(canvas); popupDiv.Controls.Add(selectionBox); // etc... add the controls to the hirearchy ... // some controls need to fire both client- and server-side events: confirmSelection.OnClientClick = "storeSelectionInfo('" + hiddenField.ClientID + "')"; confirmSelection.Click += new EventHandler(confirmSelection_Click); ... foreach (string thumbnailFilename in SessionImages) { ImageButton thumbBtn = getThumbnailButton(thumbnailFilename); thumbBtn.Command += new CommandEventHandler(btnThumb_Command); thumbnailsDiv.Controls.Add(thumbBtn); } ... thumbnailButton.Attributes.Add("onclick", "showThumbnailDiv('" + thumbnailsDiv.ClientID + "');"); uploadButton.OnClientClick = "setViewportDimensions('" + hiddenField.ClientID + "')"; this.Controls.Add(upload); this.Controls.Add(uploadButton); uploadButton.Click += new EventHandler(uploadButton_Click); ... this.ChildControlsCreated = true; }

All child controls that might be used by the control are instantiated and built into the control hierarchy in this method, even though some of them don't end up being rendered later on. Some are given specific IDs and style attributes so the rendered elements can work with the client-side JavaScript and CSS. All controls need to be present to respond to any events that might get fired a little later. CreateChildControls() will typically be called very early in the control's lifecycle, before any events are handled and before we know exactly what state the control should be in. By the time we get round to overriding the Render method later on we know exactly what the control should look like and we can selectively choose what parts of the control tree we actually want to render out to the client.

The complete control hierarchy as built by CreateChildControls() looks like this:

Control layout

Client-side JavaScript and CSS are responsible for displaying the popupDiv as a "floating" window with the selectionBox div (appearing as a dashed outline rectangle) floating above the canvas image. Once the user has already uploaded at least one image, the thumbnailsDiv element will also be rendered – this allows the user to pick a previously uploaded raw image.

The control keeps track of its current state via its controlMode property. This is an enumeration that defines at a high level the three possible states the control can be in:

public enum ControlMode { Normal, Canvas, Changed }

Normal is the starting condition. Canvas is for when the user is presented with the drawing surface and the UI is awaiting user input. If the user clicks OK and a web image is created the status will change to Changed. From the changed state the control can go back and forth between Canvas and Changed but can't return to Normal. The state can go from Normal to Canvas and back again if the user cancels in the Canvas state.

At a lower level the state of the control is kept track of by using the new ASP.NET 2.0 ControlState feature. This works very much like ViewState except that it can still be accessed when ViewState is disabled. ControlState is passed in and out of the control as a single object:

protected override void LoadControlState(object savedState)... protected override object SaveControlState()...

In this control ControlState is persisted as an array of strings. Note that we have to tell the containing page that we want to make use of its ControlState services in our OnInit method:

Page.RegisterRequiresControlState(this);

This ensures that the Load and Save method pair will be called by the Page.

Rendering

protected override void Render(HtmlTextWriter writer) { AddAttributesToRender(writer); writer.RenderBeginTag(HtmlTextWriterTag.Div); if (this.controlMode == ControlMode.Canvas) { popupDiv.RenderControl(writer) } ... ... }

In the rendering phase the control selectively asks each of its child controls to render themselves to the supplied HtmlTextWriter. The AddAttributesToRender ensures that any properties set on the control that are WebControl class properties rather than our derived WebImageMaker class properties are written out as attributes on the HTML element, which we declared in the next line to be a div element.

The popupDiv that forms the canvas UI is only rendered when the control has decided (in response to a file being uploaded and successfully read as an image) that it is in canvas mode. Similarly, the thumbnails UI is only rendered when the user has previously uploaded images:

if (SessionImages.Count > 0) { thumbnailButton.RenderControl(writer); writer.WriteBreak(); thumbnailsDiv.RenderControl(writer) }

SessionImages is a property that returns a list of thumbnail names that the control has previously generated. This list is stored in the user's session.


Navigation: Page 1 | Page 2 | Page 3 | Page 4 | Page 5 | Samples