Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

The VeryRichOutput Control

0.00/5 (No votes)
1 Aug 2011 1  
Subclass the WebBrowser control to harness the power of HTML and CSS in your output displays.

Introduction

This article outlines how to write a display control based on System.Windows.Forms.WebBrowser that uses HTML and CSS for richly formatted output. Features include:

  • HTML based output that allows for tables and other HTML functionality
  • Fully customizable CSS that gives the designer complete control over the display of text
  • Auto-scrolling to the bottom so that new text is always visible
  • A “page” queue that lets you limit how many blocks of text appear in the control

In addition, the demo code shows how to implement a custom context menu to implement Copy, Select All, Print, Print Preview and Page Setup functionality.

First.jpg

Background

In a fit of nostalgia, I started on a project to create a text-based game along the lines of Zork. With the output, though, I wanted to take advantage of the rich opportunities of a graphical interface: the text commands would be echoed in one format, room and item descriptions would be displayed in a different format, and so on.

My first attempt was to use the RichTextbox control. Within an hour, I realized that RTF was just too cumbersome to do me any good. I started searching the web for an alternative.

Then it hit me: I was searching the WEB. Web browsers already had the ability to display rich content, and with far more bells and whistles than RTF could manage. Plus, writing an HTML document was easier than writing rich text source (doing hernia surgery on yourself using a butter knife is probably easier than writing rich text source, but I digress.)

Class VeryRichOutput

The .NET Framework comes with a basic browser control, WebBrowser, which can render an HTML document with CSS. All I needed to do was add a few bits and pieces to implement helpful functionality, and I was done.

Text-based games produce a lot of output. Rather than let this grow arbitrarily large, I added the idea of “pages”, blocks of text that would be treated as a single unit. The property MaxPages defines how big the output can grow, and a Queue(Of String) stores the first-in, first-out list of pages. The method AddPage manages the queue; if MaxPages is set to a non-zero value (i.e., paging is on) and the queue has that many pages, AddPage will drop the page at the head of the queue before adding the new page to the end.

The property Style implements a List(Of String) which lets me add CSS code to the page. Flagging the property with the Editor attribute lets me tell the IDE to use the string collection editor from the Property Grid window rather than the generic list editor.

The protected method GenerateDocument assembles the styles and pages into a single HTML document, which gets passed to the DocumentText property inherited from WebBrowser. The base control lays out and renders the text to the output, then fires the OnDocumentCompleted event which has been overridden to scroll to the bottom of the document.

The last piece was to add the public method OutputPage, which gets called to send a block of text to the control.

The basic class looks like this:

Imports System.ComponentModel
Imports System.Drawing
Imports System.Text
Imports System.Windows.Forms

<DesignerCategory("code")> _
Public Class VeryRichOutput
    Inherits WebBrowser

#Region " Storage "

    Protected pMaxPages As Integer
    Protected pPages As Queue(Of String)
    Protected pStyles As List(Of String)

#End Region

#Region " Properties "

    <DefaultValue(0)> _
    <Description("The number of pages that will be displayed on a " + _
                 "first-in, first-out basis. Set to 0 for unlimited pages.")> _
    Public Property MaxPages() As Integer
        Get
            Return pMaxPages
        End Get
        Set(ByVal value As Integer)
            pMaxPages = value
        End Set
    End Property

    Protected ReadOnly Property Pages() As Queue(Of String)
        Get
            If pPages Is Nothing Then pPages = New Queue(Of String)
            Return pPages
        End Get
    End Property

    <Description("The list of styles available to the control. " + _
                 "Must be properly formatted CSS.")> _
    <Editor("System.Windows.Forms.Design.StringCollectionEditor, System.Design", _
            "System.Drawing.Design.UITypeEditor, System.Drawing")> _
    Public ReadOnly Property Styles() As List(Of String)
        Get
            If pStyles Is Nothing Then pStyles = New List(Of String)
            Return pStyles
        End Get
    End Property

#End Region

#Region " Constructors "

    Public Sub New()
        MaxPages = 0
        pPages = New Queue(Of String)
        pStyles = New List(Of String)
    End Sub

#End Region

#Region " Methods "

    Private Sub AddPage(ByVal Text As String)
        If MaxPages > 0 Then
            Do While Pages.Count >= MaxPages
                Pages.Dequeue()
            Loop
        End If

        Pages.Enqueue(Text)
    End Sub

    Protected Overridable Sub GenerateDocument()
        Dim SB As New StringBuilder

        SB.Append("<html><head><title></title>")

        SB.Append("<style type='text/css'>")
        For Each s As String In Styles
            SB.Append(s)
        Next
        SB.Append("</style></head>")

        SB.Append("<body>")
        For Each s As String In Pages
            SB.Append(s)
        Next
        SB.Append("</body></html>")

        Me.DocumentText = SB.ToString
    End Sub

    Protected Overrides Sub OnDocumentCompleted _
	(ByVal e As WebBrowserDocumentCompletedEventArgs)
        MyBase.OnDocumentCompleted(e)
        Document.Window.ScrollTo(0, Document.Body.ScrollRectangle.Height)
    End Sub

    Public Sub OutputPage(ByVal Text As String)
        AddPage(Text)
        GenerateDocument()
    End Sub

#End Region

End Class

A subclass inherits its parent’s designer as well as its code. That means VeryRichOutput would normally inherit the visual designer attached to most Controls. I find this annoying, so I use the DesignerCategory attribute to tell the IDE that the file should be treated as ordinary code and not as a control. When this control is itself subclassed, the child control also inherits the “code” designation.

WebBrowser implements a number of properties that I want to either hide or change. For example, the property AllowNavigation must be True in order to alter the base DocumentText property. To prevent this from accidentally being changed, I set the base property in the class’s constructor, then shadow the property to apply the <Browsable(False)> and <EditorBrowsable(EditorBrowsableState.Never)> attributes. I also hid the AllowWebBrowserDrop, ScriptErrorsSuppressed, Url and WebBrowserShortcutsEnabled properties, and set the IsWebBrowserContextMenuEnabled property to default to False (it is still available, though; more on that later.) The details can be seen in the source code.

Using the Control

Using the control is very simple. First, I need to add the CSS. Note that, as with any HTML document, I can modify the layout of the entire document by styling the body tag.

With BaseControl.Styles
    .Add("body {background-color:#EED;font-family:Times New Roman,serif;padding:1em;}")
    .Add(".Person {border-left:solid 3px #077;
	border-top:solid 3px #077;margin-bottom:1.5em;padding-left:0.5em;}")
    .Add(".Name {font-size:1.5em;font-weight:bold;}")
    .Add(".Addr {color:#700;}")
    .Add(".Country {color:#007;font-weight:bold;}")
End With

Next, I need to format the text before it is given to the control. I use an instance of PersonalDataClass (see project source for definition) and wrap everything up in styled HTML tags. In this code, BaseControl is the name of the VeryRichOutput control being written to.

Dim SB As New StringBuilder
SB.Append("<div class='Person'>")
SB.AppendFormat("<div class='Name'>{0} {1}</div>", PDC.FirstName, PDC.LastName)
SB.AppendFormat("<div class='Addr'>{0}<br />", PDC.Address1)
If Not String.IsNullOrEmpty(PDC.Address2) Then
    SB.AppendFormat("{0}<br />", PDC.Address2)
End If
SB.Append(PDC.City)
If Not String.IsNullOrEmpty(PDC.StateProvince) Then
    SB.AppendFormat(", {0}", PDC.StateProvince)
End If
If Not String.IsNullOrEmpty(PDC.PostalCode) Then
    SB.AppendFormat(" {0}", PDC.PostalCode)
End If
If Not String.IsNullOrEmpty(PDC.Country) Then
    SB.AppendFormat("<br /><span class='Country'>{0}</span>", PDC.Country)
End If
SB.Append("</div>") 'End Addr

SB.Append("</div>") 'End Person

BaseControl.OutputPage(SB.ToString)

Here is what the output looks after a few “Add text” clicks with the left control set to MaxPages = 3. Let’s see RichTextBox do this:

MaxPages.jpg

Subclassing the Control

VeryRichOutput, as written, is pretty basic. If you are using the class for structured data -- say, to display a look around a room or an alert that the Grue is sneaking up behind you -- you can make coding easier by subclassing it do the formatting for you.

SubclassedVeryRichOutput inherits from VeryRichOutput to implement a few additional features. It fills Styles with CSS on its own. It implements the method OutputContactInfo which takes a PersonalDataClass object, extracts the data, wraps it in HTML tags and sends it to OutputPage. It overrides the GenerateDocument method to add text to the <title> tag of the source document. (Kind of useless, but it illustrates how you can change the way the document source is created.) And lastly, it implements a custom context menu. The source code for all this can be found in the download.

WithMenu.jpg

What about "View Source" and "Find"?

I really, really would like to have these features, but Microsoft did not see fit to make them available. The base WebBrowser provides methods for print, print preview and printer setup, and the HtmlDocument property has the ExecCommand method which lets me select all text and copy selected text, but the view source and find dialogs are completely buried. Supposedly, you can use undocumented COM routines to force your way in, but I could not get those to work. If nothing else, you can view the document source by dumping DocumentText to a TextBox, or maybe add your own view source dialog.

For debugging, go ahead and set the IsWebBrowserContextMenuEnabled property to True. This will enable the standard browser context menu with the standard View Source item. It will also display a lot of other menu options you may not want to give users access to, and it will also disable any custom context menu: use with caution.

To Infinity and Beyond...

The WebBrowser control is a fully functional web browser, so there is no reason why you could not write external links or grab images or stylesheets off of the web. This approach is probably a Bad Idea: you cannot be sure that your users will have internet access, and most guardian programs get twitchy when apps suddenly start downloading things. If you want to give your users an actual web browser, give them an actual web browser.

That said, there is no reason you cannot write internal links, with anchor tags pointing to elsewhere in your output. I don’t expect there would be problems if you used the file:// protocol to import images, sound and other resources, and the availability of JavaScript opens several interesting avenues of exploration. If you experiment with this control, please post a comment and let everyone know what you learned.

In Conclusion

By harnessing the power of HTML, VeryRichOutput allows you to display richly formatted text very easily. It is important to remember that your output will have all the weaknesses of HTML as well as the strength; keep this in mind when designing your styles. If you can figure out how to implement the native View Source and Find functionality, I would really be interested in seeing your code.

History

  • Version 1 - 2011-08-01 - Initial release

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here