Introduction
Sometimes when developing a web application, you have strings that must fit within a certain pixel width (usually the width of a container, like a div, p, td, etc.). This is especially more common with links, as you sometimes need every link to fit within a single line and doesn't wrap to the next line (which will happen if the text of the link is too wide to fit within the width of the container). Or to express it visually, you might have a link that looks like this:
And want to make it look like this:
Client Side vs. Server Side
This can be done either on the client side (using CSS or JavaScript), or on the server side. For a client side solution, the CSS3 text-overflow style (text-overflow: ellipsis OR text-overflow: ellipsis-word) is the simplest and also the standard way, but unfortunately, it's still not fully supported by all browsers. There are some JavaScript solutions that can be used for now until text-overflow is fully supported; still, I prefer a server-side solution as it:
- Saves bandwidth (which can also be good for the user experience if the text is too long and takes more time to download, especially if the user has a slow connection).
- Is guaranteed to work in all browsers (including the older ones) regardless of whether they have JavaScript enabled, or even support it.
The Code
I wrapped the code required for the truncation into a class, TextTruncator
, which you can see here (also included in the download file along with a demo project):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Drawing;
namespace aspnet_TextTruncator
{
public static class TextTruncator
{
private static Dictionary<string, int[]> _fontWidthDic;
private static Dictionary<string, int[]> FontWidthDic
{
get
{
if (_fontWidthDic == null)
{
_fontWidthDic = new Dictionary<string, int[]>();
}
return _fontWidthDic;
}
}
public static string TruncateText(string text, int textMaxWidth,
string fontName, int fontSizeInPixels)
{
return TruncateText(text, textMaxWidth, fontName,
fontSizeInPixels, false);
}
public static string TruncateText(string text, int textMaxWidth,
string fontName, int fontSizeInPixels, bool isFontBold)
{
if (string.IsNullOrEmpty(text))
return text;
if (textMaxWidth < 1 ||
string.IsNullOrEmpty(fontName) ||
fontSizeInPixels < 1)
{
throw new ArgumentException();
}
int[] fontWidthArray = GetFontWidthArray(fontName,
fontSizeInPixels, isFontBold);
int ellipsisWidth = fontWidthArray['.'] * 3;
int totalCharCount = text.Length;
int textWidth = 0;
int charIndex = 0;
for (int i = 0; i < totalCharCount; i++)
{
textWidth += fontWidthArray[text[i]];
if (textWidth > textMaxWidth)
{
return text.Substring(0, charIndex) + "...";
}
else if (textWidth + ellipsisWidth <= textMaxWidth)
{
charIndex = i;
}
}
return text;
}
private static int[] GetFontWidthArray(string fontName,
int fontSizeInPixels, bool isFontBold)
{
string fontEntryName = fontName.ToLower() + "_" +
fontSizeInPixels.ToString() + "px" +
(isFontBold ? "_bold" : "");
int[] fontWidthArray;
if (!FontWidthDic.TryGetValue(fontEntryName, out fontWidthArray))
{
fontWidthArray = CreateFontWidthArray(new Font(fontName,
fontSizeInPixels, isFontBold ? FontStyle.Bold :
FontStyle.Regular, GraphicsUnit.Pixel));
FontWidthDic[fontEntryName] = fontWidthArray;
}
return fontWidthArray;
}
private static int[] CreateFontWidthArray(Font font)
{
int[] fontWidthArray = new int[256];
for (int i = 32; i < 256; i++)
{
char c = (char)i;
fontWidthArray[i] =
IsIllegalCharacter(c, false) ? 0 : GetCharWidth(c, font);
}
return fontWidthArray;
}
private static int GetCharWidth(char c, Font font)
{
return
TextRenderer.MeasureText("<" + (c == '&' ? "&&" :
c.ToString()) + ">", font).Width -
TextRenderer.MeasureText("<>", font).Width;
}
private static bool ContainsIllegalCharacters(string text,
bool excludeLineBreaks)
{
if (!string.IsNullOrEmpty(text))
{
foreach (char c in text)
{
if (IsIllegalCharacter(c, excludeLineBreaks))
return true;
}
}
return false;
}
private static bool IsIllegalCharacter(char c, bool excludeLineBreaks)
{
return
(c < 32 && (!excludeLineBreaks || c != '\n')) ||
c > 255 ||
c == 127 ||
c == 129 ||
c == 141 ||
c == 143 ||
c == 144 ||
c == 157;
}
}
}
Using the Code
The class has only one overloaded public method, TruncateText()
, which you should call to truncate the text.
Usage Example
In your .aspx page:
<%= TextTruncator.TruncateText("Some text that will " +
"likely come from your database, an XML file, or another source",
300, "Verdana", 12) %>
Notes About the Code
- To measure the text width, I use
TextRenderer.MeasureText()
, which is actually intended for Windows Forms applications, but still can be used with ASP.NET. For performance reasons, I cache the width of the letters so that I don't have to call TextRenderer.MeasureText()
again. This was done for good reasons as TextRenderer.MeasureText()
can be really slow (in my testing, this change cut the truncation time for a million strings from 8 minutes to 2 seconds!).
- Because I chose to cache the width of the letters, I had to limit the character set to ISO-8859-1 (for Latin based languages), which works well for my own purposes. If you need this code to work with Unicode, you'll need to remove the letter width caching mechanism and call
TextRenderer.MeasureText()
every time you want to measure the text width (which will slow things down a bit, but shouldn't be noticeable unless you plan to use this on a very high traffic website and on not-fast-enough machines; you'll have to do your own testing to be sure). You could also change the letter width caching mechanism to use a dictionary instead of an array, and only add the letters to the dictionary when they are actually used.
- You may notice that in
TruncateText()
, instead of passing a font object, I pass the font name, size in pixels, and whether it's bold. I do this for convenience reasons, but this should be fairly easy to change if you want to pass a Font
object (probably, to also use other font styles like italic, but you'll also need to change the caching mechanism of the letter width).
- You'll need to add references to the
System.Windows.Forms
and System.Drawing
assemblies in your project in order for this code to work.
A Note About Web Browsers
In many discussions on the web, you'll read that you shouldn't depend on the text width in pixels as there's no guarantee that different browsers will display the text in the same way (i.e., the width of the text may differ from one browser to another). While it's true that there's no guarantee, in my real life testing in all popular browsers (and less popular ones), I found that in all browsers, the width of the text as displayed in the browser never exceeded the maximum width I specified for the text in TruncateText()
; sometimes it was slightly smaller (by only a few pixels - that was in the older versions of Safari on Windows), but again, it was never larger. If you are paranoid, like me, always use a 'safety margin'. For example, if the width of the container of the text (div, p, td, or whatever element) is 500px, make the maximum width (that's passed to TruncateText()
) less by 10px or 20px, that's 490 or 480.