Displaying SharePoint Enhanced Rich Text Formatted (RTF) Text fields in Silverlight

by Aaron D-H 29. April 2009 02:56

Silverlight can be used to enhance the user experience for SharePoint users. Most data stored in SharePoint can be displayed directly by existing Silverlight controls and/or the controls available in the Silverlight Toolkit available on Codeplex.  The exception to this is the displaying of rich text formatted text field data.  RTF formatted text is stored in a simplified HTML format in SharePoint there current is no easy way to correctly display this type of text data in Silverlight… until now.

The primary mechanism for displaying text data in Silverlight is the TextBlock control.  The TextBlock control can be used for displaying text runs of changing fonts and decorations, such as bolding and underlines.  This means we can used the TextBlock control to represent “most” of the text data in a SharePoint RTF field, however, the simplified HTML used to represent RTF formated text in SharePoint can also contain hyper-links and images.  To correct represent all aspects of SharePoint RTF data we actual needed to create several different Silverlight controls.

To correctly represent RTF text data we will need to parse the HTML mark-up from in each RTF text field and translate each HTML mark-up tag into an appropriate Silverlight control.  HTML data that conforms to HTML standards also conforms to generic XML standards, so you would assume that RTF text field data could also be parsed using the built-in XML document parser.  Unfortunately the HTML fragments used by SharePoint do not conform to the latest HTML standard.  RTF text data contains many tags that are not closed.  For example,  a line break tag that conforms to the latest HTML standard looks like this “<br/>” , note the closing slash, which as pre the XML standard indicates that the br tag is closed, with no contained text or child tags.  The SharePoint RTF editor uses the HTML 1.0 version of this tag “<br>”  with no closing slash.  In fact, if you attempt to use the HTML 4.0 compliant tag “<br/>” the SharePoint text box editor will helpfully convert it back into the HTML 1.0 version “<br>”.  For HTML browsers this works just fine as they are able to easily read both HTML 1.0 and HTML 4.0 tags, however, the XML parser requires that all tags fully conform to the XML standard, which requires that all open tags have a corresponding close tag, or that the opening tag contain a closing slash like our “<br/>” example.  This of course, means that the built-in XML parse cannot be used to parse SharePoint RTF text data.

Previous attempts at creating Silverlight controls that can handle HTML data have relied on the browsers ability to parse.  This approach does work, however, in practise, the mechanism to do this appears to be very slow. 

The approach taken here is to create an HTML parser to parse the simplified HTML used by the RTF text fields.  The HTML parser will be constructed in three layers as follows:

 

Architecture

 

HtmlTextBlock Parser Layer #1

The first step in parsing text is to create the ability to read the text one character at a time, provide the ability to “look ahead” one or more character, and keep track of where the parser is in the text stream, in terms of line number and column number, in case an error or warning needs to be reported to the user.  The StreamReader class is a convenient standard class for access text data one character at a time.

In Siliverlight the following code appears to be the best way to initialize the stream reader:

private StreamReader m_streamReader; MemoryStream stream = new MemoryStream(UTF8Encoding.UTF8.GetBytes(text),false);

m_streamReader = new StreamReader(stream, UTF8Encoding.UTF8);

 

 Once the steam reader is initialized I like to wrap this layer of a text parser in a simple set of methods:

 

char nextChar() a method to return one character a time and to track the character position it terms of line number and column number.  This method also has the ability to indicated when the end of stream has been reached by returning the “character”  EOF_CHAR or ‘\0’.

void pushChar(char c)

a method to push one character back into the character stream to provide the ability for the next level of the parser to "look ahead" one or more characters.

The code for these two functions can be seen below:

        #region Parser Layer #1
        private int m_lineNumber;
        private int m_columnNumber;
        private Stack m_charStack = new Stack();
        private char EOF_CHAR = '\0';
        /// 
        /// return one character a time and to track the character position it terms of line number and column
        /// 
        /// 
        private char nextChar()
        {
            char c;
            if (m_charStack.Count &gt; 0)
            {
                c = m_charStack.Pop();
            }
            else if (m_streamReader.EndOfStream)
            {
                return EOF_CHAR;
            }
            else
            {
                c = (char)m_streamReader.Read();
            }
            if (c == '\n')
            {
                m_lineNumber++;
                m_previousLineLength = m_columnNumber;
                m_columnNumber = 0;
            }
            else
            {
                m_columnNumber++;
            }
            return c;
        }
        private int m_previousLineLength = 0;
        /// 
        /// Push one charcater back into the character stream, and adjust current character position accordingly
        /// 
        /// <param name="c" />
        private void pushChar(char c)
        {
            if (c == '\n')
            {
                m_lineNumber--;
                m_columnNumber = m_previousLineLength;
                m_previousLineLength = 80; //Supports stepping back over one line only
            }
            else
            {
                m_columnNumber--;
            }
            m_charStack.Push(c);
        }

        #endregion

Note one of the tricks used in the pushChar() method is the use of the private variable m_previousLineLength. This allows pushChar to correctly restore that correct column across one carriage return.  The method could be made more generic to allow more that one carriage return to be pushed back into the character stream, but this is not really neccsary for the type of parsing we are going to do here.

HtmlTextBlock Parser layer #2

 The next layer of our parser will be responsible for parsing the HTML tags and HTML entities such as “&amp;” etc.  I have wrapped this layer in three methods:

 

public HtmlNode NextNode() - read and remove the next node from the node stream.

public HtmlNode PeekNode() - read the next node in the node stream, but do not remove it.

public void PushNode(HtmlNode node)- Push an HtmlNode back into the node stream (in last-in-first-out order).

 

The HtmlNode class has just a few important properties.  The UML class diagram for the HtmlNode class is as follows:

HtmlNode

Type represents the type node returned, possible values are Element, EndElement, Text, Whitespace, and EOF. 

LocalName is the local name of the tag (the tag name, not including the namespace).

Prefix is the UML prefix associated with the name.  The prefix represents the short hand name of the namespace.

Value is the text value of the node, if applicable.

NormalizedValue the XML normalized version of the text value of the node, if applicable. (All whitespace is reduced to a single space).

The code for this layer can be seen here:

 

        #region Parser Layer #2
        private Stack m_nodeStack = new Stack();

        /// 
        /// Push an HtmlNode back into the node stream (in last-in-first-out order).
        /// 
        /// <param name="node" />
        public void PushNode(HtmlNode node)
        {
            m_nodeStack.Push(node);
        }
        /// 
        /// Return the next HtmlNode but do not remove it from the node stream
        /// 
        /// 
        public HtmlNode PeekNode()
        {
            HtmlNode node;
            if (m_nodeStack.Count &gt; 0)
            {
                node = m_nodeStack.Peek();
            }
            else
            {
                node = parseNode();
                m_nodeStack.Push(node);
            }
            return node;
        }
        /// 
        /// Return the next HtmlHode
        /// 
        /// 
        public HtmlNode NextNode()
        {
            if (m_nodeStack.Count &gt; 0)
            {
                return m_nodeStack.Pop();
            }
            else
            {
                return parseNode();
            }
        }
        private enum ParseModes
        {
            Whitespace = 0x01,
            Text = 0x02,
            Element = 0x04,
            Name = 0x08,
            Value = 0x10,
            QuotedValue = 0x30,

        }
        private ParseModes m_parseMode;
        private string parseEntity()
        {
            StringBuilder entity = new StringBuilder("&amp;");
            char c;
            while ((c = nextChar()) != EOF_CHAR)
            {
                if (char.IsLetterOrDigit(c) || c == '-' || c == '_')
                {
                    entity.Append(c);
                }
                else if (c == ';')
                {
                    //end of entity
                    entity.Append(c);
                    return entity.ToString();
                }
                else
                {

                    //Not an entity

                    break; 
                }
            }
            //not an entity

            for (int i = entity.Length - 1; i &gt; 0; i--)
            {
                pushChar(entity[i]);
            }
            return "&amp;";
        }
        private HtmlNode m_currentElement; //Holds turn current element node during the processing of the entire element, and it's attributes.
        private HtmlNode parseNode()
        {
            char openQuoteChar = 'x';
            StringBuilder value = new StringBuilder();
            StringBuilder localName = new StringBuilder();
            StringBuilder prefix = new StringBuilder();
            HtmlNodeType nodeType = HtmlNodeType.Unknown;
            char c;
            while ((c = nextChar()) != EOF_CHAR)
            {
                switch (c)
                {
                    case '&lt;':
                        switch (m_parseMode)
                        {
                            case ParseModes.Element:
                            case ParseModes.Name:
                            case ParseModes.Value:
                            case ParseModes.QuotedValue:
                                pushChar(c);
                                handleUnexpectedCharacter(c);
                                return new HtmlNode(nodeType, prefix.ToString(), localName.ToString(), value.ToString());
                            case ParseModes.Text:
                                if (value.Length &gt; 0)
                                {
                                    pushChar(c);
                                    return new HtmlNode(HtmlNodeType.Text, value.ToString());
                                }
                                m_parseMode = ParseModes.Element;
                                nodeType = HtmlNodeType.Element;
                                continue;
                            case ParseModes.Whitespace:
                                if (value.Length &gt; 0)
                                {
                                    pushChar(c);
                                    return new HtmlNode(HtmlNodeType.Whitespace, value.ToString());
                                }
                                m_parseMode = ParseModes.Element;
                                nodeType = HtmlNodeType.Element;
                                continue;
                        }
                        handleUnexpectedCharacter(c);
                        continue;
                    case '&gt;':
                        switch (m_parseMode)
                        {
                            case ParseModes.Element:
                                m_parseMode = ParseModes.Whitespace;
                                m_currentElement = null;
                                if (nodeType != HtmlNodeType.Unknown)
                                {
                                    return new HtmlNode(nodeType, prefix.ToString(), localName.ToString(), value.ToString());
                                }
                                continue;
                            case ParseModes.Name:
                                m_parseMode = ParseModes.Whitespace;
                                m_currentElement = null;
                                if (nodeType == HtmlNodeType.Unknown)
                                {
                                    return new HtmlNode(HtmlNodeType.Attribute, prefix.ToString(), localName.ToString());
                                }
                                else
                                {
                                    return new HtmlNode(nodeType, prefix.ToString(), localName.ToString());
                                }
                            case ParseModes.Value:
                                m_parseMode = ParseModes.Whitespace;
                                m_currentElement = null;
                                return new HtmlNode(HtmlNodeType.Attribute, prefix.ToString(), localName.ToString(), value.ToString());
                            case ParseModes.QuotedValue:
                                value.Append(c);
                                continue;
                            case ParseModes.Text:
                                if (value.Length &gt; 0)
                                {
                                    pushChar(c);
                                    return new HtmlNode(HtmlNodeType.Text, value.ToString());
                                }
                                m_parseMode = ParseModes.Element;
                                nodeType = HtmlNodeType.Element;
                                continue;
                            case ParseModes.Whitespace:
                                if (value.Length &gt; 0)
                                {
                                    pushChar(c);
                                    return new HtmlNode(HtmlNodeType.Whitespace, value.ToString());
                                }
                                m_parseMode = ParseModes.Text;
                                nodeType = HtmlNodeType.Text;
                                value.Append(c);
                                continue;
                        }
                        handleUnexpectedCharacter(c);
                        continue;
                    case '/':
                        switch (m_parseMode)
                        {
                            case ParseModes.Element:
                                if (prefix.Length == 0 || localName.Length == 0)
                                {
                                    if (nodeType == HtmlNodeType.Element)
                                    {
                                        nodeType = HtmlNodeType.EndElement;
                                        continue;
                                    }
                                }
                                else
                                {
                                    char next = nextChar();
                                    if (next == '&gt;')
                                    {
                                        m_parseMode = ParseModes.Whitespace;
                                        if (m_currentElement == null)
                                        {
                                            handleError("Implied EndElement node with no corresponding Element node", null);
                                        }
                                        else
                                        {
                                            //Create the implied EndElement node
                                            HtmlNode closeTag = new HtmlNode(HtmlNodeType.EndElement, m_currentElement.Prefix, m_currentElement.LocalName);
                                            if (nodeType == HtmlNodeType.Unknown)
                                            {

                                                return closeTag;
                                            }
                                            else
                                            {
                                                PushNode(closeTag);
                                                return new HtmlNode(nodeType, prefix.ToString(), localName.ToString(), value.ToString());
                                            }
                                        }
                                    }
                                }
                                handleUnexpectedCharacter(c);
                                continue;
                            case ParseModes.Name:
                                pushChar(c);
                                m_parseMode = ParseModes.Element;
                                return new HtmlNode(nodeType, prefix.ToString(), localName.ToString());
                            case ParseModes.QuotedValue:
                                break;
                            case ParseModes.Value:
                                pushChar(c);
                                m_parseMode = ParseModes.Element;
                                return new HtmlNode(nodeType, prefix.ToString(), localName.ToString(), value.ToString());
                        }
                        break;
                    case '"':

                    case '\'':
                        switch (m_parseMode)
                        {
                            case ParseModes.Text:
                            case ParseModes.Whitespace:
                                break;
                            case ParseModes.Value:
                                if (value.Length == 0)
                                {
                                    openQuoteChar = c;
                                    m_parseMode = ParseModes.QuotedValue;
                                    continue;
                                }
                                else
                                {
                                    m_parseMode = ParseModes.Element;
                                    pushChar(c);
                                    return new HtmlNode(HtmlNodeType.Attribute, prefix.ToString(), localName.ToString(), value.ToString());
                                }
                            case ParseModes.QuotedValue:
                                if (c == openQuoteChar)
                                {
                                    m_parseMode = ParseModes.Element;
                                    return new HtmlNode(HtmlNodeType.Attribute, prefix.ToString(), localName.ToString(), value.ToString());
                                }
                                else
                                {
                                    value.Append(c);
                                    continue;
                                }
                            case ParseModes.Name:
                            case ParseModes.Element:
                                handleUnexpectedCharacter(c);
                                continue;
                        }
                        break;
                    case '&amp;':
                        switch (m_parseMode)
                        {
                            case ParseModes.Whitespace:
                                if (value.Length &gt; 0)
                                {
                                    pushChar(c);
                                    return new HtmlNode(HtmlNodeType.Whitespace, value.ToString());
                                }
                                m_parseMode = ParseModes.Text;
                                value.Append(parseEntity());
                                continue;
                            case ParseModes.Text:
                            case ParseModes.Value:
                            case ParseModes.QuotedValue:
                                value.Append(parseEntity());
                                continue;
                        }
                        handleUnexpectedCharacter(c);
                        continue;
                    case '.':
                    case '-':
                        switch (m_parseMode)
                        {
                            case ParseModes.Whitespace:
                                if (value.Length &gt; 0)
                                {
                                    pushChar(c);
                                    return new HtmlNode(HtmlNodeType.Whitespace, value.ToString());
                                }
                                m_parseMode = ParseModes.Text;
                                value.Append(c);
                                continue;
                            case ParseModes.Text:
                            case ParseModes.Value:
                            case ParseModes.QuotedValue:
                                value.Append(c);
                                continue;
                        }
                        handleUnexpectedCharacter(c);
                        continue;
                    case ';':
                        break;

                    case ':':
                        switch (m_parseMode)
                        {
                            case ParseModes.Text:
                            case ParseModes.Whitespace:
                            case ParseModes.QuotedValue:
                                value.Append(c);
                                continue;
                            case ParseModes.Name:
                                if (prefix.Length &gt; 0)
                                {
                                    break;
                                }
                                else if (localName.Length &gt; 0)
                                {
                                    prefix.Append(localName.ToString());
                                    localName.Length = 0;
                                    continue;
                                }
                                break;
                        }
                        handleUnexpectedCharacter(c);
                        continue;
                    case '\t':
                    case ' ':
                    case '\r':
                    case '\n':
                        switch (m_parseMode)
                        {
                            case ParseModes.Text:
                            case ParseModes.Whitespace:
                            case ParseModes.QuotedValue:
                                value.Append(c);
                                continue;
                            case ParseModes.Value:
                                m_parseMode = ParseModes.Element;
                                if (localName.Length &gt; 0)
                                {
                                    return new HtmlNode(HtmlNodeType.Attribute, prefix.ToString(), localName.ToString(), value.ToString());
                                }
                                continue;
                            case ParseModes.Element:
                                continue;
                            case ParseModes.Name:
                                m_parseMode = ParseModes.Element;
                                if (nodeType == HtmlNodeType.Element || nodeType == HtmlNodeType.EndElement)
                                {
                                    m_currentElement = new HtmlNode(nodeType, prefix.ToString(), localName.ToString());
                                    return m_currentElement;
                                }
                                continue;
                        }
                        break;
                    case '=':
                        switch (m_parseMode)
                        {
                            case ParseModes.Name:
                                m_parseMode = ParseModes.Value;
                                continue;
                            case ParseModes.Text:
                            case ParseModes.Whitespace:
                            case ParseModes.QuotedValue:
                                break;
                            case ParseModes.Value:
                                if (value.Length &gt; 0)
                                {
                                    return new HtmlNode(HtmlNodeType.Attribute, prefix.ToString(), localName.ToString(), value.ToString());
                                }
                                m_parseMode = ParseModes.Element;
                                continue;
                            case ParseModes.Element:
                                handleUnexpectedCharacter(c);
                                continue;
                        }
                        break;
                }
                if (char.IsLetterOrDigit(c) || c == '_')
                {
                    switch (m_parseMode)
                    {
                        case ParseModes.Element:
                            localName.Append(c);
                            if (nodeType == HtmlNodeType.Unknown)
                            {
                                nodeType = HtmlNodeType.Attribute;
                            }
                            m_parseMode = ParseModes.Name;
                            break;
                        case ParseModes.Name:
                            localName.Append(c);
                            break;
                        case ParseModes.Value:
                        case ParseModes.QuotedValue:
                        case ParseModes.Text:
                            value.Append(c);
                            break;
                        case ParseModes.Whitespace:
                            if (value.Length &gt; 0)
                            {
                                pushChar(c);
                                return new HtmlNode(HtmlNodeType.Whitespace, value.ToString());
                            }
                            if (nodeType == HtmlNodeType.Unknown)
                            {
                                nodeType = HtmlNodeType.Text;
                            }
                            m_parseMode = ParseModes.Text;
                            value.Append(c);
                            break;
                    }
                }
                else
                {
                    //Some kind of sepparator

                    switch (m_parseMode)
                    {
                        case ParseModes.Element:
                            handleUnexpectedCharacter(c);
                            break;
                        case ParseModes.Name:
                            handleUnexpectedCharacter(c);
                            m_parseMode = ParseModes.Element;
                            break;
                        case ParseModes.Value:
                            pushChar(c);
                            m_parseMode = ParseModes.Element;
                            return new HtmlNode(HtmlNodeType.Attribute, prefix.ToString(), localName.ToString(), value.ToString());
                        case ParseModes.QuotedValue:
                        case ParseModes.Text:
                            value.Append(c);
                            break;
                        case ParseModes.Whitespace:
                            if (value.Length &gt; 0)
                            {
                                pushChar(c);
                                return new HtmlNode(HtmlNodeType.Whitespace, value.ToString());
                            }
                            m_parseMode = ParseModes.Text;
                            value.Append(c);
                            break;
                    }
                }
            }
            if (value.Length &gt; 0)
            {
                return new HtmlNode(nodeType, value.ToString());
            }
            else
            {
                return new HtmlNode(HtmlNodeType.EOF);
            }
        }
        #endregion

HtmlTextBlock Parser layer #3

The third layer of the parser reads the HTML node stream and converts it into a set of Silverlight controls.   Parsing HTML nodes into corresponding Silverlight controls is relatively straight forward if you don’t have to worry about HTML style such as padding, margins, font size and font decorations.   To simplify the parsing and displaying the correct style in Silverlight I decided to create a specialized HtmlStyle class to manage style and a set of custom Silverlight controls to mirror the functionality of each HTML tag.  The following UML class diagram represents the HtmlStyle class:

 

The main functionality of the HtmlStyle class is to replicate the cascading effect of HTML CSS style definitions.  So HtmlStyle instance are created in a parent-child hierarchy and as HTML tags are encountered the latest HtmlStyle is managed by pushing and popping HtmlStyle objects on and off of a stack.  Each attribute or property of the HtmlStyle class either has an assigned value or the value is retrieved from it’s parent HtmlStyle.

 The following table shows the HTML tag and the corresponding Silverlight control used to render it, as well as the base Silverlight class that it is derived from:

 

 

HTML TagRendering Controlbase classNotes
<a> HtmlAnchor HyperlinkButton  
<b> HtmlStyle n/a An HtmlStyle object is created with FontWieght set to Bold. 
<blockquote> HtmlBlockQuote HtmlDiv  
<br> HtmlLineBreak Control  
<div> HtmlDiv Panel  
<em> HtmlStyle n/a An HtmlStyle object is created with FontStyle set to Italic. 
<font> HtmlStyle n/a An HtmlStyle object is created with all appropriate style properties set.  If the style defines a background, and additional HtmlDiv element is created to allow background to show up correctly. 
<i> HtmlStyle n/a An HtmlStyle object is created with FontStyle set to Italic. 
<img> HtmlImg Canvas  
<p> HtmlAnchor HyperlinkButton  
<strong> HtmlStyle n/a An HtmlStyle object is created with FontWieght set to ExtraBold. 
<u> HtmlStyle n/a An HtmlStyle object is created with TextDecorations set to Underline. 
<ol> HtmlOrderedList HtmlList  
<ul> HtmlUnorderedList HtmlList  
<li> HtmlListItem StackPanel  
  HtmlList StackPanel This is the generic base class for HtmlOrderedList and HtmlListItem.
whitespace and text TextBlock / Run n/a All whitespace and text is represented using standard Silverlight TextBlock and Run classes.

 

The most interesting part of the code for level #3 can be seen here:

 

        private Stack m_elementStack = new Stack();
        UIElement m_current = null;
        TextBlock m_currentTextBlock = null;
        private void AddInline(HtmlStyle style, Inline value)
        {
            if (m_currentTextBlock == null)
            {
                TextBlock textBlock = new TextBlock();
                style.Set(textBlock);
                AddElement(textBlock);
            }
            m_currentTextBlock.Inlines.Add(value);
        }
        private void AddElement(UIElement element)
        {
            m_currentTextBlock = null;
            while (!(m_current is Panel || m_current is ContentControl))
            {
                PopElement();
            }
            if (m_current is Panel)
            {
                Panel panel = m_current as Panel;
                panel.Children.Add(element);
            }
            else if (m_current is ContentControl)
            {
                ContentControl contentControl = m_current as ContentControl;
                if (contentControl.Content == null)
                {
                    contentControl.Content = new HtmlDiv();
                }
                if (contentControl.Content is HtmlDiv)
                {
                    HtmlDiv htmlDiv = contentControl.Content as HtmlDiv;
                    htmlDiv.Children.Add(element);
                }
            }
            m_elementStack.Push(m_current);
            m_current = element;
            if (m_current is TextBlock)
            {
                m_currentTextBlock = m_current as TextBlock;
            }
        }
        private void PopElement()
        {
            m_current = m_elementStack.Pop();
            if (m_currentTextBlock != null)
            {
                m_current = m_elementStack.Pop();
                m_currentTextBlock = null;
            }
        }
        private void parseAllStyleAttributes(HtmlParser parser, HtmlStyle style)
        {
            HtmlNode node;
            while ((node = parser.NextNode()).Type == HtmlNodeType.Attribute)
            {
                Debug.WriteLine("Attribute = " + node);
                parseStyleAttribute(node,style);
            }
            parser.PushNode(node);
        }
        private void parseStyleAttribute(HtmlNode node, HtmlStyle style)
        {
            string attributeName = node.LocalName.ToUpper();
            switch (attributeName)
            {
                case ATTRIBUTE_ALIGN:
                    style.SetAlign(node.Value);
                    break;
                case ATTRIBUTE_VALIGN:
                    style.SetVAlign(node.Value);
                    break;
                case ATTRIBUTE_SIZE:
                    style.SetFontSize(node.Value);
                    break;
                case ATTRIBUTE_STYLE:
                    style.SetStyle(node.Value);
                    break;
                case ATTRIBUTE_COLOR:
                    style.SetColor(node.Value);
                    break;
            }
        }
        private void parseText(string text)
        {
            this.Children.Clear();
            m_elementStack.Clear();
            HtmlDiv root = new HtmlDiv();
            m_current = root;
            m_currentTextBlock = null;

            if (text != null)
            {
                HtmlStyle style = new HtmlStyle();
                HtmlParser parser = new HtmlParser(text);
                HtmlNode node;
                while ((node = parser.NextNode()).Type != HtmlNodeType.EOF)
                {
                    Debug.WriteLine("HtmlNode = " + node);
                    string elementName = null;
                    switch (node.Type)
                    {
                        case HtmlNodeType.Element:
                            elementName = node.LocalName.ToUpper();
                            switch (elementName)
                            {
                                case ELEMENT_A:
                                    style = new HtmlStyle(style);
                                    style.TextDecorations = System.Windows.TextDecorations.Underline;
                                    style.Foreground = new SolidColorBrush { Color = Colors.Blue };
                                    HtmlAnchor newAnchor = new HtmlAnchor();
                                    newAnchor.HtmlStyle = style;
                                    while ((node = parser.NextNode()).Type == HtmlNodeType.Attribute)
                                    {
                                        Debug.WriteLine("Attribute = " + node);
                                        string attributeName = node.LocalName.ToUpper();
                                        switch (attributeName)
                                        {
                                            case ATTRIBUTE_HREF:
                                                newAnchor.NavigateUri = new Uri(node.Value, UriKind.RelativeOrAbsolute);
                                                break;
                                            default:
                                                parseStyleAttribute(node, style);
                                                break;
                                        }
                                    }
                                    parser.PushNode(node);
                                    AddElement(newAnchor);
                                    break;
                                case ELEMENT_B:
                                    style = new HtmlStyle(style);
                                    style.FontWeight = FontWeights.Bold;
                                    break;
                                case ELEMENT_BLOCKQUOTE:
                                    style = new HtmlStyle(style);
                                    parseAllStyleAttributes(parser, style);
                                    HtmlBlockQuote newBlockQuote = new HtmlBlockQuote();
                                    newBlockQuote.HtmlStyle = style;
                                    AddElement(newBlockQuote);
                                    break;
                                case ELEMENT_BR:
                                    AddElement(new HtmlLineBreak());
                                    PopElement();
                                    break;
                                case ELEMENT_DIV:
                                    style = new HtmlStyle(style);
                                    parseAllStyleAttributes(parser, style);
                                    HtmlDiv newDiv = new HtmlDiv();
                                    newDiv.HtmlStyle = style;
                                    AddElement(newDiv);
                                    break;
                                case ELEMENT_EM:
                                    style = new HtmlStyle(style);
                                    style.FontStyle = FontStyles.Italic;
                                    break;
                                case ELEMENT_I:
                                    style = new HtmlStyle(style);
                                    style.FontStyle = FontStyles.Italic;
                                    break;
                                case ELEMENT_FONT:
                                    style = new HtmlStyle(style);
                                    parseAllStyleAttributes(parser, style);
                                    if (style.DefinesBackground)
                                    {
                                        //Can only support background color with a div
                                        HtmlDiv backgroundDiv = new HtmlDiv();
                                        backgroundDiv.HtmlStyle = style;
                                        AddElement(backgroundDiv);
                                    }
                                    break;
                                case ELEMENT_IMG:
                                    style = new HtmlStyle(style);
                                    HtmlImg newHtmlImg = new HtmlImg();
                                    while ((node = parser.NextNode()).Type == HtmlNodeType.Attribute)
                                    {
                                        Debug.WriteLine("Attribute = " + node);
                                        string attributeName = node.LocalName.ToUpper();
                                        switch (attributeName)
                                        {
                                            case ATTRIBUTE_SRC:
                                                newHtmlImg.ImageSrc = new Uri(node.Value, UriKind.RelativeOrAbsolute);
                                                break;
                                            default:
                                                parseStyleAttribute(node, style);
                                                break;
                                        }
                                    }
                                    parser.PushNode(node);
                                    newHtmlImg.HtmlStyle = style;
                                    AddElement(newHtmlImg);
                                    break;
                                case ELEMENT_LI:
                                    style = new HtmlStyle(style);
                                    HtmlListItem newListItem = new HtmlListItem();
                                    newListItem.HtmlStyle = style;
                                    AddElement(newListItem);
                                    break;
                                case ELEMENT_OL:
                                    style = new HtmlStyle(style);
                                    HtmlOrderedList newOrderedList = new HtmlOrderedList();
                                    newOrderedList.HtmlStyle = style;
                                    AddElement(newOrderedList);
                                    break;
                                case ELEMENT_UL:
                                    style = new HtmlStyle(style);
                                    HtmlUnorderedList newUnorderedList = new HtmlUnorderedList();
                                    newUnorderedList.HtmlStyle = style;
                                    AddElement(newUnorderedList);
                                    break;
                                case ELEMENT_P:
                                    style = new HtmlStyle(style);
                                    parseAllStyleAttributes(parser, style);
                                    HtmlParagraph paragraph = new HtmlParagraph();
                                    AddElement(paragraph);
                                    break;
                                case ELEMENT_STRONG:
                                    style = new HtmlStyle(style);
                                    style.FontWeight = FontWeights.ExtraBold;
                                    break;
                                case ELEMENT_U:
                                    style = new HtmlStyle(style);
                                    style.TextDecorations = TextDecorations.Underline;
                                    break;
                                default:
                                    Debug.WriteLine("Unimplemented Element = " + node);
                                    break;
                            }
                            break;
                        case HtmlNodeType.EndElement:
                            elementName = node.LocalName.ToUpper();
                            switch (elementName)
                            {
                                case ELEMENT_A:
                                case ELEMENT_BLOCKQUOTE:
                                case ELEMENT_DIV:
                                case ELEMENT_IMG:
                                case ELEMENT_LI:
                                case ELEMENT_OL:
                                case ELEMENT_P:
                                case ELEMENT_UL:
                                    style = style.Parent;
                                    PopElement();
                                    break;
                                case ELEMENT_B:
                                    style = style.Parent;
                                    break;
                                case ELEMENT_BR:
                                    AddElement(new HtmlLineBreak());
                                    PopElement();
                                    break;
                                case ELEMENT_EM:
                                    style = style.Parent;
                                    break;
                                case ELEMENT_FONT:
                                    if (style.DefinesBackground)
                                    {
                                        PopElement();
                                    }
                                    style = style.Parent;
                                    break;
                                case ELEMENT_I:
                                    style = style.Parent;
                                    break;
                                case ELEMENT_STRONG:
                                    style = style.Parent;
                                    break;
                                case ELEMENT_U:
                                    style = style.Parent;
                                    break;
                                default:
                                    Debug.WriteLine("Unimplemented EndElement = " + node);
                                    break;
                            }
                            break;
                        case HtmlNodeType.Whitespace:
                            break;
                        case HtmlNodeType.Text:
                            Run run = new Run();
                            run.Text = s_entityResolver.ResolveAllEntities(node.NormalizedValue);
                            style.Set(run);
                            AddInline(style,run);
                            break;
                    }
                }
            }
            if (root != null)
            {
                this.Children.Add(root);
            }
        }
To test this control I created a SharePoint list with an enhanced rich text field.  Here is what the column looked like on my development SharePoint site:
SharepointRTFEditor
If you look at the raw HTML generated for this column it look like this:
Rtfexample
Now here is what it looked like in an instantiation of the HtmlTextBlock control in Silverlight:
DisplayExample
Note that above UI is a little crude, the HtmlTextBlock control was used to format only the the middle part of the above image.
The complete source for the HtmlTextBlock control can be found here: 

DaisleyHarrison.Silverlight.HtmlTextBlock.v2.zip (774.80 kb)

Tags: ,

Software Engineering | C# | SharePoint | Silverlight

Comments


April 29. 2009 09:11
trackback
Trackback from DotNetKicks.com

Displaying SharePoint RTF Text field data in Silverlight


May 3. 2009 10:02
Joseph Ghassan
Very well done.

I was going to create something similar to a project I am working in.

I am trying to create a box that :

1) - I can color HTML Tags or any tag based elements in a file
2) - Ability to disable them as needed
3) - The user Can Can change text format ( B,I,U) when he highlight the text

I think your code would be a good start.


May 3. 2009 10:37
Joseph Ghassan
I added your control to a new Silverlight project and I assigned the text property of the control to a random text. Nothing is displaying in the page.

Am I missing something?

-------------------------------------------------------------------------------

<UserControl x:Class="SilverlightApplication1.MainPage"
    xmlns="schemas.microsoft.com/.../presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:controls="clr-namespaceLaughingaisleyHarrison.Silverlight.HtmlTextBlock;assembly=DaisleyHarrison.Silverlight.HtmlTextBlock"
            
    Width="400" Height="300">
    
    
        <controls:HtmlTextBlock x:Name="LayoutRoot" Text="dsdsd" Height="300" Background="Aqua" Width="300">
            
        </controls:HtmlTextBlock>
            
  
</UserControl>


May 4. 2009 04:11
aarondh
Looks like you found a bug... lol never tested the easy text.   The level 2 parser was returning EOF instead of the first text element.  
Note the posting is now updated with with fix along with the source file.  Just go ahead and download the latest, and it should work fine for your example.


May 5. 2009 19:42
Joseph Ghassan
It is working now. Thanks.

How much it is difficult though to implement the following points :

1) - I can color HTML Tags or any tag based elements or even give it a background  
2) - Ability to disable them as needed
3) - The user can change text format ( B,I,U) when he highlight the text.


May 6. 2009 00:17
aarondh
1) The parser recognizes the style attribute to allow you to change foreground and background colors.  It currently supports all of the tags used by SharePoint enhanced rich text format, except for tables.

2) Not sure what it is...  I need a more complete explanation.

3) No plans to support this. This is simply a readonly control.  Implementing this would take quite a bit of time.  If your looking for a text editor you may want to check out http://www.codeplex.com/richtextedit


United States Chad Beard 
July 7. 2009 12:07
Chad Beard
Great article!

I'm having a problem. When placing a link in the text of a paragraph it's inserting a line break before and after the link. If I remove the link from the text, it also removes the line break. Any suggestions?

Comments are closed

About the Author

I'd like to introduce myself to you... My name is Aaron Daisley-Harrison.  I have worked in the software engineering field for a number of years, and as an  Application Architect have created solutions for many industry verticals; worked with both Java and Microsoft technologies; Developed SQL database engines as well as full text search systems; and Knowledge management systems.   I am currently doing contract work out of the Pacific North West and have lately been focusing on Microsoft technologies like SharePoint 2007/2010, WCF, Identity Framework, JQuery, Xml and Silverlight.
[more]



 Digg!

 

Disclaimer
The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2009 Aaron G. Daisley-Harrison - All Rights Reserved.