Pages

Advertisement

Thursday, August 16, 2007

Using Application Configuration Files in .NET

Welcome to the next installment of the .NET Nuts & Bolts column. This article covers using application configuration files in Microsoft .NET. It briefly outlines the concept of application configuration files and touches on the native support the Microsoft .NET Framework provides. In particular, it discusses using the System.Configuration namespace among others within the Framework.

Application Configuration Background

Building configurable applications is not a new concept. The idea of applications relying on configuration parameters to dictate behavior at run time has been around for many years. For those of us who were around Microsoft-based programming prior to .NET, the most common example is the use of INI files. INI files are simple text-based files with key and value pairs; the key is used to fetch the value, and the value is then used in some way to influence the application settings and/or resulting behavior. This way, you can modify the configuration file to drive program behavior without having to recompile the application.

INI files generally worked great, except for the following drawbacks:

Microsoft moved away from INI files and towards storing everything in the Registry, which made application deployment much more difficult. As a result, Microsoft moved back to the concept of an "xcopy deployment," by which programmers deployed applications by simply copying files. Redmond used XML and some objects built into the Microsoft .NET Framework to bring new life to our old friend the application configuration file.

System.Configuration Basics

The System.Configuration namespace provides the functionality for reading configuration files. Microsoft released an XML schema dictating the format for configuration files that can be read using the System.Configuration API, enabling the accessing object to automatically consume the configuration files. This way, you can allow configuration files to be read without having to develop a bunch of plumbing to read the file and find the desired setting. Multiple types of configuration files are relevant to .NET, but this article focuses exclusively on the configuration files containing application-specific settings.

The name and storage location of an application configuration file depends on the application type with which it is being used. A configuration file for an executable (.exe) is located in the same directory as the application. The file is the name of the application with a .config extension. For example, notepad.exe would have a configuration file of notepad.exe.config. A configuration file for an ASP.NET application is called web.config and is located in the root of the application's virtual directory.

appSettings Section

An application configuration file follows a specific XML schema. The appSettings section is a predefined section of the configuration file designed to make it very easy to retrieve a value based on a given name. This is the easiest way to add application-specific settings into an application configuration file. The appSettings section of the configuration file consists of a series of "add" elements with "key" and "value" attributes. While the appSettings section is predefined, it is not included in a configuration file by default and must be manually added. A simple example of a configuration file would be the following:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   <appSettings>
      <add key="ApplicationTitle" value="Sample Console Application" />
      <add key="ConnectionString"
           value="Server=localhost;Database=Northwind;Integrated
                  Security=false;User Id=sa;Password=;" />
   </appSettings>
</configuration>

The AppSettings property of the ConfigurationSettings object in the System.Configuration namespace is used to get the settings. For example, to read either of the settings above, the following sample code would do the trick:


// Read appSettings
string title = ConfigurationSettings.AppSettings["ApplicationTitle"];
string connectString =
       ConfigurationSettings.AppSettings["ConnectionString"];

Now, you could easily set a dynamic title value or read database connection string information from the application configuration file. This gives you the freedom to adjust settings without having to recompile any code.

Customized Configuration Sections

If you have distinct groupings of configurations to be included in the application configuration file, consider creating a custom configuration section in lieu of the appSettings section. This will allow you more organizational structuring around the storage of various settings. You include a custom section by including the configSections element in the configuration file and a sub-element called section that defines the custom section and the handler for reading it. The .NET Framework contains an IConfigurationSectionHandler interface that dictates the required functionality handlers provide by allowing you to use custom handlers with the Configuration if you so choose. This example just uses handlers the .NET Framework already provides. Continuing the example from the previous section, the following configuration file contains a custom section and specifies which handler to use for reading:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   <configSections>
      <section name="sampleSection"
               type="System.Configuration.SingleTagSectionHandler" />
   </configSections>
 
   <sampleSection ApplicationTitle="Sample Console Application"
    ConnectionString="Server=localhost;Database=Northwind;
                      Integrated Security=false;User Id=sa;
                      Password=;" />
 
   <appSettings>
      <add key="ApplicationTitle"
           value="Sample Console Application" />
      <add key="ConnectionString"
           value="Server=localhost;Database=Northwind;
                  Integrated Security=false;User Id=sa;Password=;" />
   </appSettings>
</configuration>

The following code reads the configuration settings into a dictionary, where you can easily access the values:


// Read customSection
System.Collections.IDictionary sampleTable = (IDictionary)
   ConfigurationSettings.GetConfig("sampleSection");
string title = (string)sampleTable["ApplicationTitle"];
string connectString = (string)sampleTable["ConnectionString"];

 


Customized Configuration Sections and Groups

The issue with custom configuration sections alone is that they rely on attributes to store all of the settings as a part of a single element. This can become cumbersome when a large number of configurations are contained within a section or additional related sections. You can further organize similar custom sections into a section group, which makes it easier to work with numerous configurations and helps eliminate naming conflicts with custom configuration sections. This is especially useful where your application contains reference to several other DLLs that read configuration information from the host applications configuration file. A sectionGroup element is simply placed around one or more section elements to create a group. Expanding the example configuration file, the following configuration file contains a custom group, section, and the pre-defined appSettings:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   <configSections>
      <section name="sampleSection"
               type="System.Configuration.SingleTagSectionHandler" />
      <sectionGroup name="CodeGuru">
         <section name="utilitySection"
                  type="System.Configuration.NameValueSectionHandler,
                        System,Version=1.0.5000.0, Culture=neutral,
                        PublicKeyToken=b77a5c561934e089" />
      </sectionGroup>
   </configSections>
 
   <sampleSection ApplicationTitle="Sample Console Application"
    ConnectionString="Server=localhost;Database=Northwind;
                      Integrated Security=false;User Id=sa;
                      Password=;" />
 
   <CodeGuru>
      <utilitySection>
         <add key="ApplicationTitle"
              value="Sample Console Application" />
         <add key="ConnectionString"
              value="Server=localhost;Database=Northwind;
                     Integrated Security=false;User Id=sa;
                     Password=;" />
      </utilitySection>
   </CodeGuru>
 
   <appSettings>
      <add key="ApplicationTitle"
           value="Sample Console Application" />
      <add key="ConnectionString"
           value="Server=localhost;Database=Northwind;
                  Integrated Security=false;User Id=sa;Password=;" />
   </appSettings>
</configuration>

 


Note the extra "Version," "Culture," and "PublicKeyToken" specified in defining a section group. These all come from what the machine.config configures on the local machine. Occasionally, it has worked for me without them, but other times an exception is thrown that it can't create the appropriate handler. I've found that if I include those items, it consistently works.

The following code can access the items in the custom group by loading the group into a NameValueCollection for easy access to the key and value pairs:


// Read custom sectionGroup
NameValueCollection collection = (NameValueCollection)
ConfigurationSettings.GetConfig("CodeGuru/utilitySection");
string title = collection["ApplicationTitle"];
string connectString = collection["ConnectionString"];

Note the path-like statement required to find the appropriate section to read when accessing the custom section.

Possible Enhancements

This article has touched on some of the basics of using application configuration files, including the ability to define your own sections and groups of sections. Here are a couple of additional items for you to ponder about application configuration settings:

Read and Write Open XML Files in MS Office 2007

With Office 2007, Microsoft decided to change default application formats from the old, proprietary, closed formats (DOC, XLS, and PPT) to new, open and, standardized XML formats (DOCX, XLSX, and PPTX). New formats share some similarities with old Office XML formats (WordML, SpreadsheetML) and some similarities with competing OpenOffice.org OpenDocument formats, but there are many differences. Because new formats will be the default in Office 2007, and Microsoft Office is the most predominant office suite, these formats are destined to be popular and you will probably have to deal with them sooner or later.

This article will explain the basics of the Open XML file format and specifically the XLSX format, the new format for Excel 2007. Presented is a demo application that reads and writes tabular data to and from XLSX files. The application is written in C# using Visual Studio 2005. The XLSX files it creates can be opened using Excel 2007.

The Microsoft Open XML Format

Every Open XML file is essentially a ZIP archive containing many other files. Office-specific data is stored in multiple XML files inside that archive. This is in direct contrast with old WordML and SpreadsheetML formats that were single, non-compressed XML files. Although they are more complex, the new approach offers several benefits:

In Microsoft's terminology, an Open XML ZIP file is called a package. Files inside that package are called parts. It is important to know that every part has a defined content type and there are no default type presumptions based on the file extension. Content type can describe anything: application XML, user XML, images, sounds, video, or any other binary objects. Every part must be connected to some other part using a relationship. Inside the package are special XML files with a ".rels" extension that defines the relationship between parts. There is also a start part (sometimes called "root," which is a bit misleading because a graph containing all parts doesn't have to be a tree structure), so the entire structure looks like Figure 1.


(
Full Size Image)

Figure 1: Parts and relations inside an XLSX file

To make a long story short, to read the data from an Open XML file you need to:

  1. Open the package as a ZIP archive: Any standard ZIP library will do.
  2. Find the parts that contain data you want to read: You can navigate through the relationship graph (more complex), or you can presume that certain parts have a defined name and path (Microsoft can change that in the future).
  3. Read the parts you are interested in: Use the standard XML library (if they are XML) or some other method (if they are images, sounds, or some other type).

On the other hand, if you want to create a new Open XML file, you need to:

  1. Create/get all necessary parts: Use a standard XML library (if they are XML), copy them, or use some other method.
  2. Create all relationships: Create ".rels" files.
  3. Create content types: Create a "[Content_Types].xml" file.
  4. Package everything into a ZIP file with the appropriate extension (DOCX, XLSX, or PPTX): Any standard ZIP library will do.

The whole story about packages, parts, content types, and relations is the same for all Open XML documents (regardless of whether they originate in the application). Microsoft refers to this as Open Packaging Conventions.

Excel 2007 Open XML Specifics

Excel 2007 extends on the basis of Open Packaging Conventions by adding its own application-specific XML types. Reference schemas for all XML files used in Office can be downloaded from MSDN, but note that some things are still open to change until the final Excel 2007 release.

You just want to read/write worksheet data, so you need to look in the "\xl\worksheets" folder inside the XLSX file; this is where all the worksheets are located. For every worksheet, there is a separate XML file: "sheet1.xml," "sheet2.xml," and so on. When you open such a file, you will notice that all of the sheet data is inside a <sheetData> element. For every row, there is a <row> element; for every cell, there is a <c> element. Finally, the value of the cell is stored in a <v> element.

However, real-world XML is never simple as schoolbook XML. You will notice that numbers get encoded as numbers inside the <v> element:

<c r="A1">
<v>100</v>
</c>

However, a string value (like "John"), also gets encoded as a number:

<c r="B1" t="s">
<v>0</v>
</c>

That is because MS Excel uses an internal table of unique strings (for performance reasons). Zero is an index of that string in an internal table of strings and attribute t="s" tells you that the underlying type is a string, not a number. So, where is the table of unique strings located? It is in an "\xl\sharedStrings.xml" XML file, and contains all the strings used in the entire workbook, not just a specific worksheet.

This approach is used for many other things: cell styles, borders, charts, number formats, and so forth. In fact, that becomes the major programming problem when working with XLSX files—updating and maintaining various tables of some unique Excel objects. In this article, you will just read/write data values, but if you require some complex formatting you probably should use some commercial component that does all the tedious work for you.

Implementation

The demo is a Windows Forms application (see Figure 2), written in C# using Visual Studio 2005. Because there is no support for ZIP files in the .NET Framework 2.0 (only for the ZIP algorithm), the demo uses an open-source ZIP library called SharpZipLib. For demonstration purposes, you will extract entire ZIP files to a TEMP folder, so you can examine the contents of that folder and its files while debugging the demo application. In a real-world application, you may want to avoid extracting to a temporary folder and just read to/write from the ZIP file directly.

For XML processing, the choice is simple. To read XML files, you use the XmlTextReader class; to write, you use the XmlTextWriter class. Both come with the .NET Framework, but you also can use any other XML processing library.


Figure 2: Demo application in action

Data Reading

You want to read a simple "In.xlsx" file (in the "Input" folder) and copy its contents to the DataTable. That file contains a list of people with their first and last names (text values) and their IDs (number values). When the "Read input .xlsx file" button is clicked, the following code is executed:


private void buttonReadInput_Click(object sender, EventArgs e)
{
   // Get the input file name from the text box.
   string fileName = this.textBoxInput.Text;
 
   // Delete the contents of the temporary directory.
   ExcelRW.DeleteDirectoryContents(ExcelRWForm.tempDir);
 
   // Unzip the input XLSX file to the temporary directory.
   ExcelRW.UnzipFile(fileName, ExcelRWForm.tempDir);
 
   // Open the XML file with a table of all unique strings used
   // in the workbook...
   FileStream fs = new FileStream(ExcelRWForm.tempDir
                                  + @"\xl\sharedStrings.xml",
                                  FileMode.Open, FileAccess.Read);
   // ...and call a helper method that parses that XML and returns
   // an array of strings.
   ArrayList stringTable = ExcelRW.ReadStringTable(fs);
 
   // Open the XML file with worksheet data...
   fs = new FileStream(ExcelRWForm.tempDir +
                       @"\xl\worksheets\sheet1.xml",
                       FileMode.Open, FileAccess.Read);
   // ...and call the helper method that parses that XML and fills
   // DataTable with values.
   ExcelRW.ReadWorksheet(fs, stringTable, this.data);
}

Nothing unusual happens here. The XLSX file is unzipped to the TEMP folder and the necessary XML parts (now files) are processed. The "sharedStrings.xml" file contains a global table of unique strings whereas the "sheet1.xml" file contains data for the first sheet. Helper methods are pretty straightforward XML reading code—you can download demo application code to examine them in more detail.

If everything is okay, after the button is clicked, all data will show up in the DataGridView.

Data Writing

Now, you want to write data from a DataTable to the "Out.xlsx" file in the "Output" folder. You can change some data or add some new rows in the DataGridView. When the "Write output .xlsx file" button is clicked, the following code is executed:


private void buttonWriteOutput_Click(object sender, EventArgs e)
{
   // Get the output file name from the text box.
   string fileName = this.textBoxOutput.Text;
 
   // Delete the contents of the temporary directory.
   ExcelRW.DeleteDirectoryContents(ExcelRWForm.tempDir);
 
   // Unzip the template XLSX file to the temporary directory.
   ExcelRW.UnzipFile(ExcelRWForm.templateFile, ExcelRWForm.tempDir);
 
   // You will need two string tables; a lookup Hashtable for fast
   // searching and an ordinary ArrayList where items are sorted
   // by their index.
   Hashtable lookupTable;
 
   // Call the helper methods that create both tables from input
   // data.
   ArrayList stringTable = ExcelRW.CreateStringTables(this.data,
      out lookupTable);
 
   // Create an XML file...
   FileStream fs = new FileStream(ExcelRWForm.tempDir
      + @"\xl\sharedStrings.xml", FileMode.Create);
 
   // ...and fill it with unique strings used in the workbook
   ExcelRW.WriteStringTable(fs, stringTable);
 
   // Create an XML file...
   new FileStream(ExcelRWForm.tempDir + @"\xl\worksheets\sheet1.xml",
                  FileMode.Create);
 
   // ..and fill it with rows and columns of the DataTable.
   ExcelRW.WriteWorksheet(fs, this.data, lookupTable);
 
   // ZIP the temporary directory to the XLSX file.
   ExcelRW.ZipDirectory(ExcelRWForm.tempDir, fileName);
 
   // If the checkbox is checked, show the XLSX file in Excel 2007.
   if (this.checkBoxOpenFile.Checked)
      System.Diagnostics.Process.Start(fileName);
}

This time, the code is a bit more complicated. So that you don't generate all necessary parts needed for XLSX file, you should decide to use a template file. You extract the template file to the temporary folder and then just change the XML parts containing a shared string table and worksheet data. All other parts, relationships, and content types stay the same, so you don't need to generate any of that. Note that you use two string tables: a lookup Hashtable for fast searching and an ordinary ArrayList where items are sorted by their index. You could pull it out only with ArrayList, but then you would need to search entire ArrayList every time you added a new string (to check whether it is already there). The CreateStringTables() helper method builds both string tables, the WriteStringTable() helper method writes the string table XML, and the WriteWorksheet() helper method writes worksheet data XML.

Rendering Images in ASP.NET Directly from Your Database

Time and again I have read articles about loading images on ASP.NET pages. All of the articles I have read suggest a variation on storing file system paths in the database and loading the URN (or path) to the image. But why should images be treated differently than text? Images are just data and one wouldn't store paths to text or numeric data.

 

In this article, you will learn how to store images in your database, directly load them to your ASP.NET pages, and some of the benefits that can be derived from this approach. If you are sure you know how to do this, read next week's article, but consider skipping to the end of this article. You are sure to learn a couple of techniques you may not be familiar with and there is a great book recommendation.

Disclaimer: Windows Presentation Foundation (WPF, aka Silverlight) may support loading images directly from the database, when you adopt Silverlight.
Understanding What Many Programmers Do Now

Everybody uses images in ASP.NET. The de facto standard for bitmaps, JPGs, and GIFs (collectively images) in ASP.NET is to store the URL/URN to the image in the database and bind the path to the image's source property. The technique described herein is relatively easy, but almost every other kind of data is stored in the database itself and images can be too.

Loading Images Directly from Your Database

First, you know that images can be stored in the database as an Image type (at least in SQL Server, and similar types exist for Oracle or whatever). You also know that you can store Image fields and retrieve them. The trick is how to get them in the <img> HTML control or the Image Web control. Because web controls are just little HTML generators, you can use either the HTML control or the ASP.NET Image control with database image binding.

For this example, I will be using .NET 2.0, Visual Studio 2005, and SQL Server 2000. I assure you this technique works for the next version of these products too. I will also be using the AdventureWorks2000 sample database because it has images in the ProductPhoto table and should be available to many of you.

Note: The AdventureWorks2000 ProductPhoto table uses .gif images. If you try to load a .gif image directly, you will receive a System.Exception with the following error: "A Graphics object cannot be created from an image that has an indexed pixel format." This article demonstrates how to resolve this error in a couple of ways. Generally, I use JPGs because they seem to be a little more manageable here.
Storing and Retrieving Images

An insert statement will load an image to a database field. Listing 1 shows code that will load an image from a database to a custom object (in Listing 2) containing an Image field, not the byte array you get back from the database.

Listing 1: The Data Access layer that constructs the ProductPhoto generic list.

Imports Microsoft.VisualBasic
Imports System.Data
Imports System.Data.SqlClient
Imports System.Collections.Generic
 
Public Class ReadProductPhoto
 
   Private Shared connectionString As String = _
      "Data Source=BUTLER;Initial Catalog= _
       AdventureWorks2000;Integrated Security=True"
   Private Shared sql As String = _
      "SELECT ProductPhotoID, LargePhoto FROM ProductPhoto"
 
   Public Shared Function  GetProductPhotos() _
      As List(Of ProductPhoto)
 
      Using connection As SqlConnection = _
         New SqlConnection(connectionString)
         connection.Open()
         Dim command As SqlCommand = New SqlCommand(Sql, connection)
         Dim reader As SqlDataReader = command.ExecuteReader
 
         Dim list As List(Of ProductPhoto) = _
            New List(Of ProductPhoto)
         While (reader.Read())
 
            Dim id As Integer
            Dim image As Byte() = Nothing
 
            If (reader("ProductPhotoID") Is System.DBNull.Value) _
               Then Continue While
            id = reader.GetInt32(0)
 
            If (reader("LargePhoto") Is System.DBNull.Value = False) _
               Then image = CType(reader.GetValue(1), Byte())
            End If
 
            list.Add(New ProductPhoto(id, image))
 
 
         End While
 
         Return list
      End Using
   End Function
 
End Class

Listing 2: The abridged ProductPhoto class (that contains enough information for demonstration purposes).


Imports Microsoft.VisualBasic
Imports System.Drawing
Imports System.Drawing.Imaging
Imports System.IO
 
 
Public Class ProductPhoto
 
   Public Sub New(ByVal ProductPhotoID As Integer, _
                  ByVal LargePhoto As Byte())
      Me.FProductPhotoID = ProductPhotoID
      SetLargePhoto(LargePhoto)
   End Sub
 
   Private Sub SetLargePhoto(ByVal photo As Byte())
      Dim stream As MemoryStream = New MemoryStream(photo)
      LargePhoto = Image.FromStream(stream)
   End Sub
 
   Private FProductPhotoID As Integer
   Public ReadOnly Property ProductPhotoID() As Integer
      Get
         Return FProductPhotoID
      End Get
   End Property
 
   Private FLargePhoto As Image
   Public Property LargePhoto() As Image
      Get
         Return FLargePhoto
      End Get
      Set(ByVal value As Image)
         FLargePhoto = value
      End Set
   End Property
 
End Class

The key to getting the SQL Server Image type into a System.Drawing.Image type is to load a stream and construct the image from the stream, as shown in SetLargePhoto. The problem is not getting a viewable image; the problem is actually display the viewable image. If you initialize a GridView of List(Of ProductPhoto) to a GridView right now, it looks empty—no image, as illustrated in Figure 1.


Figure 1: There is no mechanism for assigning an Image field to an Image control (as represented by the Image field in the GridView shown). HTML and web controls want URLs.

Rendering an Image to a UserControl

Now that you have the Image in an object, you have to figure out how to render it. The answer is to insert the image into the HttpResponse stream. You'll add a UserControl to your project and have the UserControl render the image for you (see Listing 3).

Listing 3: You render the graphic to the HttpResponse stream in a UserControl.


Imports System.Drawing
Imports System.Drawing.Imaging
Imports System.IO
 
Partial Class ImageControl
   Inherits System.Web.UI.UserControl
 
Protected Sub Page_Load(ByVal sender As Object, _
   ByVal e As System.EventArgs) Handles Me.Load
 
   End Sub
 
   Private FImage As Image
   Public Property TheImage() As Image
      Get
         Return FImage
      End Get
      Set(ByVal value As Image)
         FImage = value
      End Set
   End Property
 
   Private Sub RenderImage()
#Const BestForGif = True
   If (FImage Is Nothing) Then Return
#If BestForGif Then
      ' this approach seems to work best for gifs - so we are
      ' switching back to a byte array - jpegs can use the code below
      Dim stream As MemoryStream = New MemoryStream()
      FImage.Save(stream, ImageFormat.Gif)
      Response.BinaryWrite(stream.ToArray())
#ElseIf ResolvesGifPoorQuality Then
 
      ' resolves: "A Graphics object cannot be created from
      ' an image that has an indexed pixel format too" for gifs
      ' but yields a poor result. Use for jpegs or bmps
 
      Dim b As Bitmap = New Bitmap(FImage.Width, FImage.Height)
      Dim g As Graphics = Graphics.FromImage(b)
      g.DrawImage(FImage, 0, 0)
      Response.ContentType = "image/gif"
      b.Save(Response.OutputStream, ImageFormat.Gif)
      Response.End()
 
#Else    ' simple for something like gifs
 
      Response.ContentType = "image/jpeg"
      FImage.Save(Response.OutputStream, IMageFormat.Jpeg)
      Response.End
#End If
 
   End Sub
 
   Protected Sub Page_PreRender(ByVal sender As Object, _
      ByVal e As System.EventArgs) Handles Me.PreRender
      RenderImage()
   End Sub
End Class

After adding a UserControl to your project, add an Image property and a method that sends the image to the HttpResponse stream. RenderImage shows a couple variations of the image rendering. GIFs use indexes into a color palette, so the best result seems to be simply adding the raw bytes to the response stream. If you use something like JPG files, you can use the last #Else condition block, which is pretty straightforward.


Placing the UserControl on a Designated Page

You have the UserControl. Now, youneed a page. The reason for the UserControl and Page is that writing to the HttpResponse stream directly is destructive; if you wrote right to the page that ultimately will contain your images, you would wipe everything else out of the page. (I suspect there is a way to inject the image in a non-destructive way, but I haven't quite perfected it yet.)

 


The next step is to add a page, drop the UserControl on the page, and coordinate associating each unique ID with the UserControl on each page. Listing 4 contains the code for the page that will play the role of the image source.

Listing 4: The Page_Load event coordinates which image is shown in any specific page and UserControl pair.


Imports System.Drawing
Imports System.Drawing.Imaging
Imports System.Collections.Generic
 
Partial Class ProductImage
      Inherits System.Web.UI.Page
 
Protected Sub Page_Load(ByVal sender As Object, _
   ByVal e As System.EventArgs) Handles Me.Load
 
      If (Request.QueryString("id") Is Nothing) Then Return
      FId = CType(Request.QueryString("id"), Integer)
      Dim list As List(Of ProductPhoto) = CType(Session("data"), _
         List(Of ProductPhoto))
      Dim o As ProductPhoto = list.Find(AddressOf Match)
      If (o Is Nothing = False) Then
         FImage = o.LargePhoto
      End If
 
   End Sub
 
   Private FId As Integer
 
   Private Function Match(ByVal o As ProductPhoto) As Boolean
      Return o.ProductPhotoID = FId
   End Function
 
   Private FImage As Image
   Public Property TheImage() As System.Drawing.Image
      Get
         Return FImage
      End Get
      Set(ByVal value As System.Drawing.Image)
        FImage = value
      End Set
   End Property
 
 
   Protected Sub Page_PreRender(ByVal sender As Object, _
      ByVal e As System.EventArgs) Handles Me.PreRender
      ImageControl1.TheImage = FImage
   End Sub
 
End Class

The Page_Load event is looking for a QueryString indexed by the string "id". The QueryString—~/ProductImage.aspx?id=1, for example—indicates which key is used to determine which record this page instance is responsible for. Next, you get the List(Of ProductPhoto) objects from session, and finally use List(Of T).Find to search the list for the page's ProductPhoto. Find takes a predicate—think a where condition—and sets the found Image property of the contained Image control.



Figure 2: A visualization depicting the relationship between the page containing the rendered image and the <img> control that uses the page as the image source.

Loading the Page as an Image Control's Source

The final step is to get a list of ProductPhoto objects, stuff those in session, and associate a dynamic path with the ImageUrl (or src) attribute of the image control. Typically, the ImageUrl (or src) attributes have been images; however, by using a page you effectively can obtain a dynamic palette on which to draw. Listing 5 shows a page that consumes the image loaded from a database, and Listing 6 shows the ASPX that binds a page path with the query string. Finally, the rendered images are shown in Figure 3.

Listing 5: A .aspx page that contains a GridView and the images loaded from the database.


Imports System.Web.Compilation
Imports System.Collections.Generic
 
Partial Class _Default
      Inherits System.Web.UI.Page
 
   Protected Sub Page_Load(ByVal sender As Object, _
      ByVal e As System.EventArgs) Handles Me.Load
 
      If (IsPostBack) Then Return
      Dim list As List(Of ProductPhoto) = _
         ReadProductPhoto.GetProductPhotos()
      Session("data") = list
      GridView1.DataSource = list
      GridView1.DataBind()
 
   End Sub
 
   Protected Function GetUrl(ByVal obj As Object) As String
      Dim o As ProductPhoto = CType(obj, ProductPhoto)
      Return String.Format("~/ProductImage.aspx?id={0}", _
                           o.ProductPhotoID)
   End Function
 
End Class
Listing 6: The HTML/ASPX that shows the GridView control and the binding statement that invokes the GetUrl method in Listing 5.


<%@ Page Language="VB" AutoEventWireup="false"
         CodeFile="Default.aspx.vb" Inherits="_Default" %>
 
<%@ Register Src="ImageControl.ascx" TagName="ImageControl"
             TagPrefix="uc1" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
   <title>Untitled Page</title>
</head>
<body>
   <form id="form1" runat="server">
   <div>
      <asp:GridView ID="GridView1" runat="server"
                    AutoGenerateColumns="False" Width="100%">
         <Columns>
            <asp:BoundField DataField="ProductPhotoID"
                            HeaderText="ID" />
            <asp:TemplateField HeaderText="Large Photo">
               <EditItemTemplate>
               </EditItemTemplate>
               <ItemTemplate>
                  <asp:Image ID="Image1" runat="server"
                     ImageUrl='<%# GetUrl(Container.DataItem) %>' />
               </ItemTemplate>
            </asp:TemplateField>
         </Columns>
      </asp:GridView>
      &nbsp;
 
   </div>
   </form>
</body>
</html>


Figure 3: The AdventureWorks2000 product photos rendered from the database.

Deriving Additional Benefits

So, now you know you can store and render images right from a database. This capability needs to be componentized (by Microsoft). An additional benefit is that, because you are rendering the database images from code, you can draw, paint, annotate, scale, rotate (or whatever you want to do to) the images because you effectively have a canvas (hDC, DC, or Graphics object, whichever term you are familiar with) to draw on.

Listing 7 demonstrates how you can scale the image, maintaining the aspect ratio by setting the largest side (height or width) to the new desired maximum size and then scaling the other dimension proportionately. Listing 8 shows how you can write on the image, supporting the idea of dynamic annotation, perhaps showing a sales price.

Listing 7: Code that scales the images (but keep in mind that the GIF images didn't render well after manipulation; the fault may be mine though).


Const max  As Integer = 64
Dim width  As Integer
Dim height As Integer
Dim scalar As Single
 
If (FImage.Width > FImage.Height) Then
   width  = max
   scalar = width / FImage.Width
   height = scalar * FImage.Height
Else
   height = max
   scalar = height / FImage.Height
   width  = scalar * FImage.Width
End If
 
Dim bmp As Bitmap = New Bitmap(width, height)
Dim g As Graphics = Graphics.FromImage(bmp)
g.DrawImage(FImage, 0, 0, bmp.Width, bmp.Height)
Dim stream As MemoryStream = New MemoryStream()
bmp.Save(stream, ImageFormat.Gif)
Response.BinaryWrite(stream.ToArray())

 

Listing 8: You have a perfectly valid canvas to write on, so you can use and of the GDI+ capabilities when you manage loading the images from the database.

 


Dim b As Bitmap   = New Bitmap(FImage.Width, FImage.Height)
Dim g As Graphics = Graphics.FromImage(b)
g.DrawImage(FImage, 0, 0)
Dim f As Font = New Font(FontFamily.GenericSansSerif, 12, _
                         FontStyle.Italic)
g.DrawString("Extra Information", f, Brushes.Red, 10, 10)
Dim stream As MemoryStream = New MemoryStream()
b.Save(stream, ImageFormat.Gif)
Response.BinaryWrite(stream.ToArray())

 

Finally, it is worth noting that you could store the path to the image in the database, use Image.LoadFromStream—Image.LoadFromFile locks images—and manipulate the image. Then, you could use the techniques described in this article to render the image. The main premise is why should images be stored differently when they don't have to be?