Build A Self-Caching ASP.NET Web Site Thumbnail Control

Peter hooks up his new WebSiteThumbnail class into the previously published In-Memory Image control, and shows how it works.

"Clearly the use of a mobile phone while driving can be a distraction to drivers, but labeling it the only . . . <<<CRASHHHHHH>>>" -- Anonymous Expert

Don't ask me why, but recently I got involved in finding out how to generate web site "snapshot" thumbnail images like the ones you see on Alexa or via the snap.com freebie.

I didn't particularly plan on using this stuff on any of my sites, but I guess I was just curious. Turns out that with .NET, I could only find one fairly reliable way to take a picture of a web page and it has some "gotchas". This will illustrate what I did to fix them. For starters, I should mention that what I present here is an extension of my in memory ASP.NET image control here. It just made sense that once I figured out a reliable way to get a snapshot" of a web page, I'd want to put it into an image control that offers caching. In addition, I've set it up so that you can use the class that does the Web site snapshots in a manner external to the control, and I've added two static methods, one of which allows the specification of an absolute path for saving the generated thumbnail images. If an image is requested that already exists, the cached version is served, which is much faster.

At any rate, the trick is to use the Windows Forms Webbrowser control. Contrary to what you may or may not have heard, this does not have to be situated on a container form - it can be used independently with 100% programmatic access. There are a couple of catches: First, being a Windows Form control, this must operate on an STA (Single Threaded Apartment) thread. This means you need to either set the "AspCompat = "true" attribute on the page that uses it, or you need to make the actual Webbrowser Navigate call to the target page on a secondary thread whose state has been set to STA. I chose the latter. The other gotcha is that the Webbrowser control does its navigation on more than one thread. The DocumentCompleted event handler is fired when the browser has fully loaded the target url, and therefore it is in this event that we want to do our business logic.

In this case, I decided to use a ManualResetEvent to block the initial calling thread until the bitmap is actually available, and then call the Set method on it in order to unblock and allow the method to return our web site thumbnail. Concurrent with this, I use a timeout that also sets the ManualResetEvent since occasionally the Webbrowser control will go into LaLa Land especially if you are behind a corporate firewall, whose software brand I will choose not to mention. In the event that the returned bitmap is null, I then replace it with a stock image "Photo Not Available" that I pull out of the assembly as an embedded resource. The last thing you want to see is an ugly red X image placeholder in the browser.

Since the code for the Image control is already published, here I'll just focus on the part that I added to it, my "WebSiteThumbnail" class:

using System;
using System.Drawing;
using System.Windows.Forms;
using System.Threading;
using System.IO;
using System.Reflection;

namespace PAB.WebControls
{
    public class WebSiteThumbnail
    {
        private string url = null;      
        private Bitmap bmp = null;
        public Bitmap Image 
        {
            get 
            { 
                return bmp; 
            } 
        }
        private ManualResetEvent mre = new ManualResetEvent(false);
        private int timeout = 5; // this could be adjusted up or down
        private int thumbWidth;
        private int thumbHeight;
        private int width;
        private int height;
        private string absolutePath;
        #region Static Methods
        public static Bitmap GetSiteThumbnail(string url, int width, int height, int thumbWidth, int thumbHeight)
        {
            WebSiteThumbnail thumb = new WebSiteThumbnail(url, width, height, thumbWidth, thumbHeight);
            Bitmap b = thumb.GetScreenShot();
            if (b == null)
                b = (Bitmap)System.Drawing.Image.FromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream("PAB.WebControls.Notavailable.jpg"));
            return b;
        }

        public static Bitmap GetSiteThumbnail(string url, int width, int height, int thumbWidth, int thumbHeight,string absolutePath)
        {           
            WebSiteThumbnail thumb = new WebSiteThumbnail(url, width, height, thumbWidth, thumbHeight,absolutePath );
            Bitmap b = thumb.GetScreenShot();
            if (b == null)
                b = (Bitmap)System.Drawing.Image.FromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream("PAB.WebControls.Notavailable.jpg"));
             return b;
        }
        #endregion

        #region Ctors
        public WebSiteThumbnail(string url, int width, int height,int thumbWidth, int thumbHeight)
        {
            this.url = url;
            this.width = width;
            this.height = height;
            this.thumbHeight = thumbHeight;
            this.thumbWidth = thumbWidth;
        }
        public WebSiteThumbnail(string url, int width, int height, int thumbWidth, int thumbHeight,string absolutePath)
        {
            this.url = url;
            this.width = width;
            this.height = height;
            this.thumbHeight = thumbHeight;
            this.thumbWidth = thumbWidth;
            this.absolutePath = absolutePath;
        }
        #endregion

        #region ScreenShot
        public Bitmap GetScreenShot()
        {
             string fileName = url.Replace("http://", "") + ".jpg";
             fileName = System.Web.HttpUtility.UrlEncode(fileName);
            if (absolutePath != null &&  File.Exists(absolutePath + fileName))
            {                
                bmp = (Bitmap)System.Drawing.Image.FromFile(absolutePath + fileName);                   
            }
            else
            {
                Thread t = new Thread(new ThreadStart(_GetScreenShot));
                t.SetApartmentState(ApartmentState.STA);
                t.Start();
                mre.WaitOne();
                t.Abort();
            }
            return bmp;
        }
        #endregion
        private void _GetScreenShot()
        {
            WebBrowser webBrowser = new WebBrowser();
            webBrowser.ScrollBarsEnabled = false;
            DateTime time = DateTime.Now;
            webBrowser.Navigate(url);
            webBrowser.DocumentCompleted += new WebBrowserDocumentCompletedEventHandler(WebBrowser_DocumentCompleted);
            while (true)
            {
                Thread.Sleep(0);
                TimeSpan elapsedTime = DateTime.Now - time;
                if (elapsedTime.Seconds >= timeout)
                {
                    mre.Set();
                }
                Application.DoEvents();
            }


        }
        private void WebBrowser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
        {
            WebBrowser webBrowser = (WebBrowser)sender;
            webBrowser.ClientSize = new Size(this.width, this.height);
            webBrowser.ScrollBarsEnabled = false;
            bmp = new Bitmap(webBrowser.Bounds.Width, webBrowser.Bounds.Height);
            webBrowser.BringToFront();
            webBrowser.DrawToBitmap(bmp, webBrowser.Bounds); 
            Image img = bmp.GetThumbnailImage(thumbWidth, thumbHeight, null, IntPtr.Zero);
            string fileName = url.Replace("http://", "") + ".jpg";
            fileName = System.Web.HttpUtility.UrlEncode(fileName);
          if (absolutePath != null && !File.Exists(absolutePath + fileName))
          {
              img.Save(absolutePath + fileName);
          }  
            bmp = (Bitmap)img;           
            webBrowser.Dispose();
          if (mre != null) 
              mre.Set();
        }

        public void Dispose()
        {
            if (bmp != null) this.bmp.Dispose();
        }
    }
}
The downloadble solution includes a page that uses the Control, and another page that calls an "imagehandler" page with the parameters on the querystring and shows an image just using the class itself (from out of the control assembly) with the images being cached in a "thumbnails" folder.  Threading aficionados may notice that I am calling Abort on the background thread after the ManualReset is released. I know setting IsBackGround and using  Join() would be a lot gentler, and I tried it. Sorry, the call to Abort on the thread has to stay, at least for now.

There is proxy code in Global.asax that reads from entries in the web.config if you are behind a firewall. No entries, no proxy. Enjoy.

Download the Visual Studio 2005 Solution accompanying this article.
By Peter Bromberg   Popularity  (19256 Views)