Display Images in a DataGrid Directly
from Memory with no Http Handlers

by Peter A. Bromberg, Ph.D.


Peter Bromberg

"The trouble with being poor is that it takes up all of your time."  - Willem de Kooning

You can find a number of solutions around that illustrate techniques for displaying images from a database in your DataGrid or other databound control, but they almost universally resort to the trick of having the image source set to an external handler or page that receives the image ID on the querystring, pulls it out of the database, and writes it into the Response stream.

That's fine, if that's what you want to do, but it's a particularly inefficient way to do things especially if you already have the ability to include the image column along with whatever else your SQL query is getting out of your database. In other words, why make a separate HTTP request AND a separate database query for each image, when you can already have them sitting in a column of your DataTable in the first place, just waiting to be used?

Here I present a solution that incorporates two separate techniques:

1) A custom Image control where we can set the bitmap property at runtime, avoiding the overhead of each img tag making its own request to another page and potentially necessitating another database query, and

2) A technique to assign the image column values in the ItemDataBound handler directly from your Datatable while the DataGrid is performing its databinding operation.



First, let's look at some simple code for the control. Once you understand the beauty of the technique, I'm sure you will be able to come up with other uses for it as well:

using System;
using System.Collections.Specialized;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.Design;
using System.Web.UI.WebControls;
using System.Reflection ;
using System.ComponentModel;

namespace PAB.WebControls
{
 public enum ImageType
 {
         Gif,
   Jpeg
 }

 public enum Persistence
 {
  Cache,
  Session   
 }
  
 [Designer("PAB.WebControls.ImageControlDesigner"),
ToolboxDataAttribute("<{0}:ImageControl Runat=\"server\"></{0}:ImageControl>")] public class ImageControl : Control { protected string ImageUrl; private ImageType imageType; [Description("Image Type")] [Category("Data")] [DefaultValue("Gif")] [Browsable(true)] public ImageType ImageType { get { return imageType; } set { imageType=value; } } private Persistence persistenceType; [Description("Cache or Session Persistence")] [Category("Data")] [DefaultValue("Cache")] [Browsable(true)] public Persistence PersistenceType { get { return persistenceType; } set { persistenceType=value; } } private Bitmap _bitmap; [Browsable(false)] public Bitmap Bitmap { get { if(this.PersistenceType==Persistence.Session ) return (Bitmap)Context.Session[String.Concat(CreateUniqueIDString(), "Bitmap")]; else return (Bitmap)Context.Cache[String.Concat(CreateUniqueIDString(), "Bitmap")]; } set { if(this.PersistenceType==Persistence.Session) Context.Session[String.Concat(CreateUniqueIDString(), "Bitmap")] = value; else Context.Cache[String.Concat(CreateUniqueIDString(), "Bitmap")] = value; } } private string CreateUniqueIDString() { string idStr=String.Empty; string tmpId=String.Empty; if( this.PersistenceType ==Persistence.Session ) { idStr = "__" +Context.Session.SessionID.ToString() +"_"; } else { if(Context.Cache["idStr"]==null) { tmpId=Guid.NewGuid().ToString(); Context.Cache["idStr"]=tmpId; } idStr= "__"+Context.Cache["idStr"].ToString() +"_"; } idStr = String.Concat(idStr, UniqueID); idStr = String.Concat(idStr, "_"); idStr = String.Concat(idStr, Page.ToString()); idStr = String.Concat(idStr, "_"); return idStr; } private void ImageControl_Init(EventArgs e) { HttpRequest httpRequest = Context.Request; HttpResponse httpResponse = Context.Response; if (httpRequest.Params[String.Concat("ImageControl_", UniqueID)] != null) { httpResponse.Clear(); if(this.ImageType==ImageType.Gif) { httpResponse.ContentType = "Image/Gif"; Bitmap.Save(httpResponse.OutputStream,ImageFormat.Gif ); } else { httpResponse.ContentType = "Image/Jpeg"; Bitmap.Save(httpResponse.OutputStream, ImageFormat.Jpeg); } httpResponse.End(); } string str = httpRequest.Url.ToString(); if (str.IndexOf("?") == -1) { ImageUrl = String.Concat(str, "?ImageControl_", UniqueID, "=1"); } else { ImageUrl = String.Concat(str, "&ImageControl_", UniqueID, "=1"); } } protected override void OnInit(EventArgs e) { ImageControl_Init(e); } protected override void Render(HtmlTextWriter output) { output.Write("<img id={0} src={1}>", this.UniqueID,ImageUrl ); } } public class ImageControlDesigner: System.Web.UI.Design.ControlDesigner { public ImageControlDesigner(){} public override string GetDesignTimeHtml() { return GetEmptyDesignTimeHtml (); } protected override string GetEmptyDesignTimeHtml() { return CreatePlaceHolderDesignTimeHtml( "<div>[Image is set at runtime. Place
control inside Table TD or DIV for absolute positioning.]</div>"
); } } }

It can be seen above that essentially what this control does is provide a server-side Bitmap property to hold an actual image, along with the required IMG src url designed not to actually point to an image resource, but more to serve as a means of storing and retrieving the image by UniqueId (that's important) of the control, from either Cache or Session.

I use either the SessionID or a Guid in string form to help create this "phantom url".

Now we come to our Page code. I have a simple DataGrid with a Name column, and a Template Column that holds an instance of my ImageControl:

<asp:DataGrid id="DataGrid1" style="Z-INDEX: 101; LEFT: 352px; POSITION: absolute; TOP: 216px"
runat="server" AutoGenerateColumns="False">
<Columns>
<asp:BoundColumn DataField="name" HeaderText="Name"></asp:BoundColumn>
<asp:TemplateColumn HeaderText="Image">
<ItemTemplate>
<cc1:ImageControl id="ImageControl1" Runat="server"></cc1:ImageControl>
</ItemTemplate>

<EditItemTemplate>
<asp:TextBox id="TextBox1" runat="server"></asp:TextBox>
</EditItemTemplate>
</asp:TemplateColumn>
</Columns>
</asp:DataGrid>

We get our sample images from the trusty old Northwind Employees table like so:

	  private void Page_Load(object sender, System.EventArgs e)
		{
			SqlConnection cn;
			cn = new SqlConnection
				("DATABASE=northwind;SERVER=localhost;UID=sa;");
			String cmdText = "SELECT top 4 lastname as name, photo as image FROM Employees";			
			SqlDataAdapter da = new SqlDataAdapter(cmdText,cn);
			DataSet ds=new DataSet();
			da.Fill(ds);
			DataTable tbl = ds.Tables[0];			
			DataGrid1.DataSource=tbl;
			DataGrid1.DataBind();
		}
	  
	  

Now comes the last trick, in the ItemDataBound event handler:

	  private void DataGrid1_ItemDataBound(object sender, DataGridItemEventArgs e)
		{			
		MemoryStream ms=null;		
		PAB.WebControls.ImageControl ctrl=null;
			byte[] b=null;
			if ( e.Item.ItemType == ListItemType.AlternatingItem ||
				   e.Item.ItemType == ListItemType.Item)
			{
			b = (byte[])(DataBinder.Eval(e.Item.DataItem, "image"));				
			ms = new MemoryStream();
				// 78 is the size of the OLE header for Northwind images, need to strip it off				
			int offset = 78; 				
			ms.Write(b, offset, b.Length-offset);
			ctrl = (PAB.WebControls.ImageControl)e.Item.FindControl("ImageControl1");
			ctrl.Bitmap=(Bitmap)System.Drawing.Image.FromStream(ms);					
			}
		}

We need a MemoryStream for this operation. If the item being databound is a regular data display row, we cast the Item.DataItem "image" DataRowView Column to a byte array. In this particular case, Images in the Northwind Employees table were designed from MS Access, which expects a 78 byte OLE header, which we'll strip off. We write the rest of the array into our MemoryStream. Finally, we cast the e.Item.FindControl("ImageControl1") to an instance of my PAB.WebControls.ImageControl, assign the Image to it's Bitmap property, and -- voila! here's what we get:

I've been trying to get Nancy Davolio's home phone number for years, but apparently, she's too involved with Andrew Fuller, so I guess I'm out of luck. Apparently (for those who are curious) there is some mystique to the Nancy Davolio mystery. Mike Gunderloy explains:

"As far as I know, Nancy Davolio originated in Access 1.0. The *name* was made up, but the *photo* has always been of a real Microsoft employee - not, however, with that name. (The same applies to the rest of the people in the Northwind sample database: synthetic names, real people).

The name has turned up in tons of examples because Microsoft legal cleared it for use in such things (they have a big master list they maintain for just that purpose)."

Shucks. Access 1.0? She's probably retired by now, another Microsoft millionairess with all that stock. Oh, well....

The full solution for the control and the web page is available for download here.

 


 

Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform.