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.