Click here to Skip to main content
15,867,568 members
Articles / Desktop Programming / Windows Forms

WinForms WebBrowser + HTTP Server

Rate me:
Please Sign up or sign in to vote.
5.00/5 (26 votes)
18 Apr 2010CPOL3 min read 74.5K   4.8K   62   10
A tiny HTTP server wrapped in a WebBrowser control

Image 1

WebBrowserEx Demo screenshot

Table of Contents

Introduction

This is a short article that fixes the problems with using the WinForms WebBrowser.DocumentText property.

I have written a tiny HTTP server and have wrapped it in a class derived from WebBrowser. This means that instead of setting the WebBrowser.DocumentText property with your HTML, you can call WebBrowser.Navigate( [server url] ) and let the server provide your HTML and supporting files just like a real website. This makes the WebBrowser control much happier.

Background

I have been using a WinForms WebBrowser control as the display in an HTML authoring application. The HTML changes with every keystroke, so I need to send it to the control dynamically. I was using the WebBrowser.DocumentText property, but this was causing all sorts of little problems.

Problems

Relative Paths

Firstly, the HTML had <img> and <a> tags that pointed at files on disk with paths relative to the saved HTML document. This shows an example directory structure:

C#
+ WebBrowserEx              // parent directory
   + Article                // base directory
      + WebBrowserEx.html   // html file
      + WebBrowserEx        // subdirectory
         + demo.png         // image file
         + src.zip          // zip file
   + WebBrowserEx           // code directory
      + WebBrowserEx.sln    // solution file
      + ...

So the HTML would look like this:

HTML
<img src='WebBrowserEx/demo.png'>
<a href='WebBrowserEx/src.zip'>

I fixed this by modifying the HTML that was sent to the control by adding a <base> tag to the <head> element:

XML
<base href='file:///C:/...path.../WebBrowserEx/Article/'>

This fixed the images, but for some reason ( security? ) not the links. This was the best compromise workaround I could find at the time.

Internal Links

The other problem was with anchors. The HTML usually had a "Table of Contents", which was basically a list of links to anchors:

HTML
...
<h2>Table of Contents</h2>
<ul>
  <li><a href='#Introduction'>Introduction</a></li>
...
<h2><a name='Introduction'>Introduction</a></h2>
...

With the <base> tag needed to show the images, the anchor links were combined to include the file:// protocol, giving URLs like this:

file:///C:/...path.../WebBrowserEx/Article/#Introduction

These don't point to the correct locations and so none of the internal links worked.

Solution

So, all in all, using WebBrowser.DocumentText caused unacceptable compromises about which tags worked and which didn't.

Having accepted that the WebBrowser control was not ever going to be happy without a proper HTTP server, I wondered how difficult it would be to write one. It turned out that it was ridiculously easy.

The .NET Framework has included the difficult part since version 2.0: implementing the HTTP/1.1 protocol. The relevant classes are in the System.Net namespace in System.dll:

C#
class HttpListener           // handles socket-level connections
class HttpListenerContext    // holds the request and response objects
class HttpListenerRequest    // parses the request into properties
class HttpListenerResponse   // compiles the response into valid HTTP
Stream response.OutputStream // holds the response entity in binary

All I had to do was use the URL to either return the HTML string or find a file on disk and write them as binary to the OutputStream. OK, I did also have to set a few header properties, for example ContentType, but really: it's amazing how much knowledge is coded in the Framework!

Using the Code

If your requirements match mine, all you have to do is replace your WebBrowser instance with a WebBrowserEx and call LoadHtml( string html, string baseDirectory ) instead of setting DocumentText. That's it.

The basic server code is very simple. If your requirements are slightly different, all you need to do is change where you get the binary data. For instance, if your images are stored as resources in your assembly, you can serve them from there. No more funny URIs and Base64 encoding!

Full Source

I wouldn't normally dump the entire source in the article itself, but since it's only 200 lines in total:

C#
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Mime;
using System.Text;
using System.Threading;

namespace Common
{
  public class WebBrowserEx : System.Windows.Forms.WebBrowser
  {
    string _Html = String.Empty;
    string _BaseDirectory = String.Empty;

    public void LoadHtml( string html, string baseDirectory )
    {
      if ( SynchronizationContext.Current != _UI )
        throw new ApplicationException(
          "WebBrowserEx.LoadHtml must be called on the UI thread." );

      InitServer();

      _Html = html;
      _BaseDirectory = baseDirectory;

      if ( _Server == null ) return;

      _Server.SetHtml( html, baseDirectory );

      Navigate( _Server.Url );
    }

    SynchronizationContext _UI = null;
    bool _TriedToStartServer = false;
    Server _Server = null;

    public WebBrowserEx()
    {
      _UI = SynchronizationContext.Current;
    }

    void InitServer()
    {
      if ( _TriedToStartServer ) return;

      _TriedToStartServer = true;

      _Server = new Server();
    }

    public class Server
    {
      string _Html = String.Empty;
      string _BaseDirectory = String.Empty;

      public void SetHtml( string html, string baseDirectory )
      {
        _Html = html;
        _BaseDirectory = baseDirectory;
      }

      public Uri Url { get { return new Uri(
        "http://" + "localhost" + ":" + _Port + "/" ); } }

      HttpListener _Listener = null;
      int _Port = -1;

      public Server()
      {
        var rnd = new Random();

        for ( int i = 0 ; i < 100 ; i++ )
        {
          int port = rnd.Next( 49152, 65536 );

          try
          {
            _Listener = new HttpListener();
            _Listener.Prefixes.Add( "http://localhost:" + port + "/" );
            _Listener.Start();

            _Port = port;
            _Listener.BeginGetContext( ListenerCallback, null );
            return;
          }
          catch ( Exception x )
          {
            _Listener.Close();
            Debug.WriteLine( "HttpListener.Start:\n" + x );
          }
        }

        throw new ApplicationException( "Failed to start HttpListener" );
      }

      public void ListenerCallback( IAsyncResult ar )
      {
        _Listener.BeginGetContext( ListenerCallback, null );

        var context = _Listener.EndGetContext( ar );
        var request = context.Request;
        var response = context.Response;

        Debug.WriteLine( "SERVER: " + _BaseDirectory + " " + request.Url );

        response.AddHeader( "Cache-Control", "no-cache" );

        try
        {
          if ( request.Url.AbsolutePath == "/" )
          {
            response.ContentType = MediaTypeNames.Text.Html;
            response.ContentEncoding = Encoding.UTF8;

            var buffer = Encoding.UTF8.GetBytes( _Html );
            response.ContentLength64 = buffer.Length;
            using ( var s = response.OutputStream ) s.Write( buffer, 0, buffer.Length );

            return;
          }

          var filepath = Path.Combine( _BaseDirectory,
            request.Url.AbsolutePath.Substring( 1 ) );

          Debug.WriteLine( "--FILE: " + filepath );

          if ( !File.Exists( filepath ) )
          {
            response.StatusCode = ( int ) HttpStatusCode.NotFound; // 404
            response.StatusDescription = response.StatusCode + " Not Found";

            response.ContentType = MediaTypeNames.Text.Html;
            response.ContentEncoding = Encoding.UTF8;

            var buffer = Encoding.UTF8.GetBytes(
              "<html><body>404 Not Found</body></html>" );
            response.ContentLength64 = buffer.Length;
            using ( var s = response.OutputStream ) s.Write( buffer, 0, buffer.Length );

            return;
          }

          byte[] entity = null;
          try
          {
            entity = File.ReadAllBytes( filepath );
          }
          catch ( Exception x )
          {
            Debug.WriteLine( "Exception reading file: " + filepath + "\n" + x );

            response.StatusCode = ( int ) HttpStatusCode.InternalServerError; // 500
            response.StatusDescription = response.StatusCode + " Internal Server Error";

            response.ContentType = MediaTypeNames.Text.Html;
            response.ContentEncoding = Encoding.UTF8;

            var buffer = Encoding.UTF8.GetBytes(
              "<html><body>500 Internal Server Error</body></html>" );
            response.ContentLength64 = buffer.Length;
            using ( var s = response.OutputStream ) s.Write( buffer, 0, buffer.Length );

            return;
          }

          response.ContentLength64 = entity.Length;

          switch ( Path.GetExtension( request.Url.AbsolutePath ).ToLowerInvariant() )
          {
            //images
            case ".gif": response.ContentType = MediaTypeNames.Image.Gif; break;
            case ".jpg":
            case ".jpeg": response.ContentType = MediaTypeNames.Image.Jpeg; break;
            case ".tiff": response.ContentType = MediaTypeNames.Image.Tiff; break;
            case ".png": response.ContentType = "image/png"; break;

            // application
            case ".pdf": response.ContentType = MediaTypeNames.Application.Pdf; break;
            case ".zip": response.ContentType = MediaTypeNames.Application.Zip; break;

            // text
            case ".htm":
            case ".html": response.ContentType = MediaTypeNames.Text.Html; break;
            case ".txt": response.ContentType = MediaTypeNames.Text.Plain; break;
            case ".xml": response.ContentType = MediaTypeNames.Text.Xml; break;

            // let the user agent work it out
            default: response.ContentType = MediaTypeNames.Application.Octet; break;
          }

          using ( var s = response.OutputStream ) s.Write( entity, 0, entity.Length );
        }
        catch ( Exception x )
        {
          Debug.WriteLine( "Unexpected exception. Aborting...\n" + x );

          response.Abort();
        }
      }
    }
  }
}

Conclusion

Well, that's about it. I hope you have enjoyed reading this article and can use this technique in your code.

Please leave a vote and/or a comment below. Thanks.

History

  • 18th April, 2010: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United Kingdom United Kingdom
I discovered C# and .NET 1.0 Beta 1 in late 2000 and loved them immediately.
I have been writing software professionally in C# ever since

In real life, I have spent 3 years travelling abroad,
I have held a UK Private Pilots Licence for 20 years,
and I am a PADI Divemaster.

I now live near idyllic Bournemouth in England.

I can work 'virtually' anywhere!

Comments and Discussions

 
QuestionCannot stop listener Pin
lightfinder4-Sep-14 5:05
lightfinder4-Sep-14 5:05 
QuestionHi! Pin
GigaZiv26-Jun-14 19:06
GigaZiv26-Jun-14 19:06 
GeneralMy vote of 5 Pin
latencymachine16-May-12 4:55
latencymachine16-May-12 4:55 
Yes! Just the technique we may have been looking for. Thanks for sharing this idea!
Generalwrong path when the css file is not on root folder Pin
Poletto1-Feb-11 0:18
Poletto1-Feb-11 0:18 
GeneralMy vote of 5 Pin
antomic0713-Jan-11 9:24
antomic0713-Jan-11 9:24 
GeneralThank you! Pin
Member 437224217-Aug-10 4:45
Member 437224217-Aug-10 4:45 
GeneralExcellent! Pin
JamesMMM20-Apr-10 3:25
JamesMMM20-Apr-10 3:25 
GeneralRe: Excellent! Pin
Nicholas Butler20-Apr-10 5:08
sitebuilderNicholas Butler20-Apr-10 5:08 
GeneralNice! Pin
Tiep Le20-Apr-10 2:51
Tiep Le20-Apr-10 2:51 
GeneralRe: Nice! Pin
Nicholas Butler20-Apr-10 3:21
sitebuilderNicholas Butler20-Apr-10 3:21 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.