DataGridViewHTMLCell – Displaying HTML markup in a DataGridView

Displaying HTML markup in a DataGridView

I recently wanted to display some HTML content in a DataGridView cell. However, after searching the web I wasn’t able to find anything that fitted my needs (i didn’t find anything). So I decided to investigate what was involved in writing a custom DataGridView Cell.

Basically to create your own custom cell you need to create / derive from three base classes to do with the DataGridView control. These are:

  • Derive a class from DataGridViewColumn
  • Derive a class from DataGridViewCell
  • Derive a class that implements the IDataGridViewEditingControl interface

Below is a screen shot of the demo application.

The source code / sample application for this project can be found on GitHub here: https://github.com/OceanAirdrop/DataGridViewHTMLCell

HTML Renderer

To render the HTML content in the cell I will be using the HTMLRenderer library. This is a fantastic framework. It is lightweight, includes its own rendering engine (no external dependencies) and includes only 2 .DLL’s. Check it out here: https://htmlrenderer.codeplex.com/ for more information.

To add HTMLRenderer to your project either use nuget to add the references automatically or download the .dll’s from codeplex/github and reference them in your C# project manually.

Deriving from DataGridViewColumn (Step 1 of 3)

Let’s first look at the 1st of the 3 classes that need to be derived from: The DataGridViewColumn class. MSDN states that the DataGridViewColumn class “represents a logical column in a DataGridView control”. This class is needed and provides scaffolding for the DataGridView to work. It lets the DataGridView know what type of cell is in that column.

Here is the class in its entirety:

Code Snippet 1: Setting up the DataGridViewHTMLColumn

public class DataGridViewHTMLColumn : DataGridViewColumn
{
    public DataGridViewHTMLColumn() : base(new DataGridViewHTMLCell())
    { 
    }

    public override DataGridViewCell CellTemplate
    {
        get
        {
            return base.CellTemplate;
        }
        set
        {
            if (!(value is DataGridViewHTMLCell))
                throw new InvalidCastException("CellTemplate must be a DataGridViewHTMLCell");

            base.CellTemplate = value;  
        }
    }
}

Short and sweet eh? Not much to see here. – Basically we have created a class named: DataGridViewHTMLColumn. Notice in the constructor we are new-ing up a DataGridViewHTMLCell (marked in red). We are also overriding the CellTemplate property. The CellTemplate property “sets the template used to create new cells.”

Deriving from DataGridViewCell (Step 2 of 3)

This is where most of our custom code will live. The DataGridViewCell class represents an individual cell in the DataGridView control. The bulk of our code will reside in this class.

Let’s take a look at the Paint method:

Code Snippet 2: DataGridViewCell Paint Method

protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates cellState, object value, 
                              object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, 
                              DataGridViewPaintParts paintParts)
{
    base.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState, null, null, errorText, cellStyle, advancedBorderStyle, paintParts);

    Image img = GenerateHTMLImage(rowIndex, value, base.Selected);

    if (img != null)
    {
        graphics.DrawImage(img, cellBounds.Left, cellBounds.Top);
    }
}

The purpose/output of the Paint function is to draw the HTML on screen. The line: graphics.DrawImage(img, cellBounds.Left, cellBounds.Top); actually does the drawing but the main bulk of the work is sub-contracted out to another function: GenerateHTMLImage(). This function generates the HTML image which will be painted to screen in the location of the cell.

Let’s take a closer look at the GenerateHTMLImage() function:

Code Snippet 3: GenerateHTMLImage Function

private Image GenerateHTMLImage(int rowIndex, object value, bool selected)
{
    // Step 1: Get Size of DataGridView Cell.
    Size cellSize = GetSize(rowIndex);

    if (cellSize.Width < 1 || cellSize.Height < 1)
        return null;

    // Step 2: Set HTML Text to render.
    m_editingControl.Size = GetSize(rowIndex);
    SetHTMLPanelText(m_editingControl, Convert.ToString(value));

    if (m_editingControl != null)
    {
        // Step 3: Render HTML Image.
        Image htmlImage = null; Size fullImageSize;

        if ( RenderHTMLImage( Convert.ToString(value), cellSize, out htmlImage, out fullImageSize ) == false )
        {
            Console.WriteLine("Failed to Generate HTML image");
            return null;
        }

        // Step 4: If necessary, add an elipsis (...) to image.
        if ( fullImageSize.Height > cellSize.Height ) 
        {
            // there is more html than being displayed!! Lets add some elipsis (...) to the image
            // to let the user know there is more to display

            htmlImage = AddElipsisToImage(htmlImage);
        }

        return htmlImage;
    }

    return null;
}

Following along with the red comments in the code snippet above, the 1st thing we do is get the size of the cell. If the width or height is less than 1 we simply return null as there is nothing worth painting

In step 2, we then set the HTML’s “.Text” property (so it knows what HTML to render). The “value” variable is the actual html markup.

At step 3, we call the RenderHTMLImage() function. This returns an image representation of the HTML to be displayed.

Finally, at step 4, we check to see if we need to add an ellipsis to the image. More on this later on.

Let’s now take a look at the RenderHTMLImage() function:

Code Snippet 4: Actually render the HTML image

private bool RenderHTMLImage(string htmlText, Size cellSize, out Image htmlImage, out Size fullImageSize)
{
    bool bResult = true;

    // Step 1: Set out parameters
    htmlImage = null;
    fullImageSize = new System.Drawing.Size();
            
    try
    {
        // Step 2: Check for null html text
        if (string.IsNullOrEmpty(htmlText) == true)
            htmlText = "This cell has a null value!";

        // Need to render image twice! Once to get the full size of image and once to get image clipped to the size of the cell

        // Step 3: Render the html image using the full height but keep the cell width.
        htmlImage = HtmlRender.RenderToImage(htmlText, maxWidth: cellSize.Width);

        // Step 4: Keep a record of the full image size to send back to caller.
        fullImageSize.Height = htmlImage.Height;
        fullImageSize.Width = htmlImage.Width;

        // Step 5: Render the HTML imaage a second time with the cell size width / height
        htmlImage = HtmlRender.RenderToImage(htmlText, new Size(cellSize.Width, cellSize.Height)); 

        m_editingControl.Text = htmlText;
    }
    catch(Exception ex)
    {
        bResult = false;
    }
            
    return bResult;
}

The main bits of code to pick out here are in steps 3 and 5. Essentially we are calling through to the 3rd party HTMLRenderer library to render the HTML as an image.

htmlImage = HtmlRender.RenderToImage(htmlText, new Size(cellSize.Width, cellSize.Height));

The above line is where we are leveraging the functionality from the HTMLRenderer library.

Implementing the IDataGridViewEditingControl interface (Step 3 of 3)

At first I did not implement this but it meant that users could not interact with this control. The cell didn’t feel natural. The user could not copy the text in the cell to the clipboard. I then (originally) played with using a simple TextBox control but this displayed the full markup text (which is something I suspect you don’t want your users to see). Let’s take a look at the code:

Code Snippet 5: Setting up the DataGridView Editing Control

public class DataGridViewHTMLEditingControl : HtmlLabel, IDataGridViewEditingControl
{
    private DataGridView m_dataGridView;
    private int m_rowIndex;
    private bool m_valueChanged;

    public DataGridViewHTMLEditingControl()
    {
        this.BorderStyle = BorderStyle.None;
        AutoSize = false;
    }
}

In a nutshell the code for this class looks like the code snippet above (I have left out the overridden interface methods for brevity). The important thing to note is that this class is-a HtmlLabel. We derive from the HtmlLabel class. Therefore DataGridViewHTMLEditingControl is-a HtmlLabel. To satisfy the requirements of being an editable DataGridView control we also need to implement the methods in the interface IDataGridViewEditingControl. There is nothing exciting in these interface methods which is the reason I have not displayed them here. You can view them over at the github page referenced above.

All done!

At this point we are all done. That’s it. We have implemented all 3 classes needed to create a custom cell in the DataGridView control. It nicely displays html content. But before wrapping up I just wanted to discuss the ellipsis code mentioned above.

Extra: Adding an Ellipsis (…) to bottom right of Cell

I noticed, when playing around with the HTML cell that, if there was more text to display the cell didn’t indicate anything back to the user to let them know “there was more”. I then created a simple text cell and filled it with random text and noticed it displayed an ellipsis (…) to tell the user: “Hey! There is more text for me to display if you care to resize me”. This is where the ellipsis comes in.

In the code snippet 4 above you may have noticed that I was rendering the html image twice and thought to yourself “why would you do that?”. Well, the first time I render the image I let the HTMLRenderer render the full text. This produces an image with the full HTML in. I then render the HTML a 2nd time but this time limit the image size to the cell width and height. Now, we can check if the full image size height is greater than the cell image size height. If it is, we know there is more HTML to display and can add an ellipsis to the cell.

The code that adds the ellipsis is as follows:

Code Snippet 6: Adding an elipsis to the cell

Image AddElipsisToImage(Image img)
{
    // This function will grab a graphics object from the image and then draw another image on-top
    Graphics g = Graphics.FromImage(img);

    // elipsis
    g.DrawImage(Resources.elipsis, new Point(img.Width - Resources.elipsis.Width, img.Height - Resources.elipsis.Height));

    return img;
}

The only caveat I have found with this is that when the cell is in edit mode the control is painted by itself which means (obviously) the ellipsis is not displayed. But that is to be expected.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.