Rendering a Custom Control to an Image

July 21, 2009
Sean Cooper

We've built a number of internal tools to support our SEO business. One of those tools displays a very simple bar graph. While the control worked great, we realized that a picture of the graph would be great for including in the reports we give to our clients.

Of course, my first thought was to use the graphics libraries in .NET and write a bunch of code to create an image of the graph. However, some quick searches in Google pointed me towards a completely different way of obtaining a picture of our already functioning graph.

Browser Power

The article that really got me going was about getting a thumbnail image of a webpage. It showed me how the WebBrowser class could be taken advantage of to get a graphic from HTML. The concept works like this:
1) get the control to render itself into HTML
2) capture the HTML
3) feed it to the WebBrowser object
4) generate a bitmap from the WebBrowser object.

I had the idea that we could use the WebBrowser control to render an image of our control.

Starting the Control

Let's start with a new class that inherits from the WebControl class.


Imports System
Imports System.Collections.Generic
Imports System.ComponentModel
Imports System.Text
Imports System.Web
Imports System.Web.UI
Imports System.Web.UI.WebControls
Imports System.Drawing
Imports GraphControl.My.Resources
Imports System.Windows.Forms
Imports System.Threading

Namespace MiniGraph
   
    Public Class MiniGraph
        Inherits WebControl  
       
        Private _graphStyle As GraphStyleType
        Private _tableStyle As New TableItemStyle
        Private _onStyle As New Style
        Private _offStyle As New Style
        Private _cellHeight As Integer
        Private _cellWidth As Integer
        Private _bitmap As Bitmap
        Private mre As ManualResetEvent = New ManualResetEvent(False)                      
                       

Take special note of a couple namespaces that we're importing:
System.Threading and System.Windows.Forms. We're importing the System.Threading namespace because the WebBrowser class has to be run on an STA thread. The Windows.Forms namespace gives us access to the WebBrowser object.
We also create our class-level Bitmap field as well as a ManuarRaiseEvent object that we'll use later.

Rendering the Control

We're going to override the Render and RenderContents methods of our control so we can have full control over the HTML that is output. In our case, we're building a table and adding styles to the elements, then calling the RenderControl method of the table to fill the HTMLTextWriter with all the happy HTML .NET can render. We've also created a property named HTML that gives us access to the rendered HTML of the control.


Protected Overrides Sub RenderContents(ByVal writer As System.Web.UI.HtmlTextWriter)
    Dim tb As New Table
    tb.ApplyStyle(_tableStyle)
    Dim i As Integer
    Select Case Orientation
        Case OrientationType.Horizontal
            i = 1
            tb.Rows.Add(New TableRow)

            While i <= 10
                Dim td As New TableCell
                td.Text = " "
                If i < round() Then
                    td.ApplyStyle(_onStyle)
                Else
                    td.ApplyStyle(_offStyle)
                End If
                tb.Rows(0).Cells.Add(td)
                i += 1
            End While
        Case OrientationType.Vertical
            i = 10
            While i >= 1
                Dim tr As New TableRow
                Dim td As New TableCell
                If i > round() Then
                    td.ApplyStyle(_offStyle)
                Else
                    td.ApplyStyle(_onStyle)
                End If
                tr.Cells.Add(td)
                tb.Rows.Add(tr)
                i -= 1
            End While
    End Select

    tb.RenderControl(writer)
End Sub

Protected Overrides Sub Render(ByVal writer As System.Web.UI.HtmlTextWriter)
    RenderContents(writer)
End Sub            
 
Public ReadOnly Property HTML() As String
    Get
        Dim stringWriter As New System.IO.StringWriter
        Dim htmlWriter As New HtmlTextWriter(stringWriter)
        Me.Render(htmlWriter)
        Return stringWriter.ToString
    End Get
End Property

Get the Picture?

Take a look at the code and then we'll talk about what's going on.


Public Function ToImage() As Bitmap

    Dim t As New Thread(New ThreadStart(AddressOf GetImage))
    t.SetApartmentState(ApartmentState.STA)
    t.Start()
    mre.WaitOne()
    t.Abort()
    Return _bitmap

End Function

Private sub GetImage()
    Dim myRectangle As New Rectangle
    Dim Generator As New WebBrowser
    Dim mystring As String = "<html><body>" & HTML & "</body></html>"
    myRectangle.X = 0
    myRectangle.Y = 0
    Select Case Orientation
        Case OrientationType.Horizontal
            myRectangle.Width = CellWidth * 11
            myRectangle.Height = CellHeight
        Case OrientationType.Vertical
            myRectangle.Width = CellWidth
            myRectangle.Height = CellHeight * 11
    End Select

    Generator.DocumentText = mystring

    While Generator.DocumentText <> mystring
        Thread.Sleep(100)
        Application.DoEvents()
    End While

    Generator.ClientSize = New Size(myRectangle.Width * 5, myRectangle.Height * 5)
    _bitmap = New Bitmap(myRectangle.Width * 3, CInt(myRectangle.Height * 1.5))
    Generator.ScrollBarsEnabled = False
    Generator.BringToFront()
    Generator.DrawToBitmap(_bitmap, Generator.Bounds)
    Generator.Dispose()
    If Not mre Is Nothing Then mre.Set()
End Sub

In the ToImage function, we simply create a new STA thread with the address of our GetImage routine and kick it off. We use the WaitOne method of the ManualRaiseEvent to block our thread until the new thread is done processing.

Within GetImage we create our WebBrowser object. We also create a simple HTML string using the HTML property of our class. Once we have the HTML, we assign it to the DocumentText property of the WebBrowser and wait for it to process. It is key to call the Application.DoEvents in order to unblock the thread the WebBrowser is running on and give it time to process the DocumentText we fed it.

Once the WebBrowser has finished processing the document, we instatiate a Bitmap of the desired size, turn of the scoll bars, and call the DrawToBitmap method of the WebBrowser, passing our _bitmap object byref. Just to make sure everything is tidy, we dispose of the WebBrowser. Finally, we call the Set method of the ManualRaiseEvent to unblock the parent thread which then aborts the STA thread and returns our bitmap.

Using the Image

Of course, this is all academic if you don't have a place to use it, right? In our case, this graph will go into a report we give our clients. A big portion of the report is generated from a single web page.

Let's create a basic web page with the control on a page that will return the image.


<%@ Page Language="VB" AutoEventWireup="true" CodeFile="Default.aspx.vb" 

    Inherits="_Default" %>



<%@ Register Namespace="GraphControl.MiniGraph" 

    TagPrefix="MGR" Assembly="GraphControl" %>

    

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" 

    "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

    

<html xmlns="http://www.w3.org/1999/xhtml">

<head id="Head1" runat="server">

    <title>Untitled Page

</head>

<body>

    <form id="form1" runat="server" enableviewstate="True">

    <div>

        <MGR:MiniGraph runat="server" ID="graph1" Datapoint="35" 

            Orientation="Vertical" CellHeight="10" CellWidth="10" />

        

    </div>

    </form>

</body>

</html>                        

                        

                        

Nothing special there. Just registering a control on a page.

In our code behind for default.aspx, there shouldn't be any surprises either.


    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) _
        Handles Me.Load

        Dim outImage As Bitmap = graph1.ToImage

        Context.Response.ContentType = "Image/Jpeg"
        Dim fmt As ImageFormat = ImageFormat.Jpeg
        outImage.Save(Context.Response.OutputStream, fmt)
        Context.Response.End()
    End Sub    

In this case, on the Load event, we get the image from the control, set our response type and write out the image.

Conclusion

And there you have it! How to take a control and make it render itself into an image. This techinque could be adapted to make a full page render itself into an image, if you had the need. While this specific technique may have limited uses, it does show how adapting controls not normally used within ASP.Net can add power to your pages. Enjoy.