A Silverlight TagCloud, Part 1: The WCF Service

July 23, 2009 09:44

The complete Silverlight TagCloud series of posts:

  1. A Silverlight TagCloud, Part 1: The WCF Service
  2. A Silverlight TagCloud, Part 2: The TagCloud
  3. A Silverlight TagCloud, Part 2.1: Refinements
  4. Silverlight TagCloud Now on CodePlex

This is the first of two posts about a new Silverlight TagCloud control I wrote. The architecture is simple: On the client-end we'll have a Silverlight control hosted in an ASP.NET page. On the server-end we'll have a WCF service that the TagCloud communicates with to get tag item information.

Here's the end result (minus some CSS formatting):

image

Since I knew I wanted to deploy the control/service combination to two different web sites, I opted to create both control and service projects in a new Visual Studio 2008 solution rather than adding them to each of the individual web projects where I'd have duplication of code. Turns out the process of getting the WCF bindings, service config, placement of the Silverlight XAP file, references, etc. all working correctly together when the service and control are not located inside the same solution as the web project was a major chore. Long story short, I figured it all out; I'll spare you my tales of woe and just give you the solution.

In this first post I intend to discuss the WCF service. The next post in the series will cover the Silverlight control.

Let me also add that since I use BlogEngine.NET as my hosting platform that the WCF service is tightly-bound with BlogEngine.NET's Post class. Replacing this underlying post/tag store shouldn't be that difficult, though, if you want to adapt this to your own blogging platform.

The TagCloudService

I started with a WCF Service Library since my thought was that a service library would make deployment to two different web sites easier (you can read about the differences between the WCF Service Application and Service Library templates here). Turns out it wasn't any easier or harder than if I'd just started with a WCF Service Application. In any case, I started with the usual Visual Studio 2008 WCF Service Library template:

image

After some renames and the addition of a TagCloudService.svc file (which you would have gotten for free using the WCF Service Application template), I wound up with this structure:

image

You'll notice that the TagCloudService.cs file is not tucked behind the TagCloudService.svc file in the usual code-behind manner. This is because the content of the .svc file is this:

  1: <%@ ServiceHost Language="C#" Debug="true" Service="TagCloud.TagCloudService.TagCloudService" %>

 

No CodeBehind attribute. This is so because I'll be deploying the compiled service DLL as the service's implementation, and not releasing the .cs file at all.

The Contract

Let's take a look at the service contract. Here's the entire file:

   1: using System.Collections.Generic;
   2: using System.Runtime.Serialization;
   3: using System.ServiceModel;
   4:  
   5: namespace TagCloud.TagCloudService
   6: {
   7:     [ServiceContract (Namespace = "TagCloud.TagCloudService")]
   8:     public interface ITagCloudService
   9:     {
  10:         [OperationContract]
  11:         List<CloudTag> GetTags (int threshold, string baseUrl);
  12:     }
  13:  
  14:     [DataContract]
  15:     public class CloudTag
  16:     {
  17:         public CloudTag (string tagName, string tagLink, int tagOccurrences)
  18:         {
  19:             TagName = tagName;
  20:             TagLink = tagLink;
  21:             TagOccurrences = tagOccurrences;
  22:         }
  23:  
  24:         [DataMember]
  25:         public string TagName;
  26:  
  27:         [DataMember]
  28:         public string TagLink;
  29:  
  30:         [DataMember]
  31:         public int TagOccurrences;
  32:     }
  33: }

The service contract defines just one operation: GetTags. It takes as parameters the tag occurrence threshold, which is a minimum number of times a tag must have been used in our blog before we'll display it in the TagCloud control, and the baseUrl, which is the hosting web site's base address, as in http://www.itscodingtime.com. We'll see how the base url is used in a moment. GetTags returns a collection of CloudTag objects.

CloudTag is defined with the [DataContract] attribute; it's members are [DataMember]'s. CloudTag holds relevant information for a single tag on our blog, including the name of the tag, the url to the tag so that visitors can click on a tag in the cloud and be taken to the corresponding posts, and the number of occurrences of the tag.

The Implementation

The service implementation is rather short:

   1: using System.Collections.Generic;
   2: using System.ServiceModel.Activation;
   3: using BlogEngine.Core;
   4:  
   5: namespace TagCloud.TagCloudService
   6: {
   7:     [AspNetCompatibilityRequirements (RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
   8:     public class TagCloudService : ITagCloudService
   9:     {
  10:         public List<CloudTag> GetTags (int threshold, string baseUrl)
  11:         {
  12:             var cloudTagCollection = new List<CloudTag> ();
  13:  
  14:             if (Post.Posts == null)
  15:             {
  16:                 return (cloudTagCollection);
  17:             }
  18:  
  19:             // get all tags and the number of times each occurs
  20:             var sortedDict = new SortedDictionary<string, int> ();
  21:             foreach (var post in Post.Posts)
  22:             {
  23:                 if (!post.IsVisible) continue;
  24:  
  25:                 foreach (var tag in post.Tags)
  26:                 {
  27:                     if (sortedDict.ContainsKey (tag))
  28:                         sortedDict[tag]++;
  29:                     else
  30:                         sortedDict[tag] = 1;
  31:                 }
  32:             }
  33:  
  34:             // we have all the tags. only save those that meet our minimum occurrences threshold
  35:             foreach (var tag in sortedDict)
  36:             {
  37:                 if (tag.Value > threshold)
  38:                 {
  39:                     cloudTagCollection.Add (new CloudTag (tag.Key, baseUrl + "/?tag=/" + tag.Key, tag.Value));
  40:                 }
  41:             }
  42:  
  43:             return (cloudTagCollection);
  44:         }
  45:     }
  46: }

 

The main part is, of course, the GetTags operation, which utilizes BlogEngine.NET's Posts collection to retrieve all tags and the number of times each appears across the blog. It then adds only those tags that meet the minimum threshold to the CloudTag collection. This collection is then what is returned to the consuming app, which in this case will be the Silverlight control.

Deployment

You can deploy the WCF service right now (since nothing is actually using it yet). I've written a couple of posts on how to do that, but what you basically need to do is make the needed changes to your web.config, then copy over the .svc and assembly DLL. You can then access the service to make sure it's running with, for example, http://www.itscodingtime.com/TagCloudService.svc.

Conclusion

As you can see the WCF service part of the implementation is pretty straightforward. Deployment of the service is (hopefully) no more difficult.

The second part of this post will discuss the consumer of this service: the Silverlight TagCloud control. Until then.

Download: Silverlight TagCloud control on CodePlex


Silverlight Live Comment Preview Control

April 17, 2009 12:38

The complete Silverlight Live Comment Preview series of posts:

  1. Silverlight Live Comment Preview Control
  2. Silverlight Live Comment Preview Control: Some Touch-up and Now on CodePlex

A short time ago I noticed a problem with the AJAX live comment scripting on my other site (it most likely occurred as a result of my latest BlogEngine.NET upgrade). This was preventing people who were leaving blog comments from being able to preview their comments and any formatting they might have applied before hitting "Save". Rather than fix the problem with the scripting, I looked at it instead as a great opportunity to re-implement the functionality as a Silverlight control.

The basic functionality of this control is this: take user input in real-time from an ASP.NET TextBox control and display it in a TextBlock hosted by the Silverlight control. The user has the option of using formatting tags: [b][/b] for bolded text, [q][/q] for quoted text (displayed in a different font than the other text; suitable for displaying blocks of code and such), [i][/i] for italicized text, and [u][/u] for underlined text.

The end result looks something like:

image

image

There were a couple of challenges in developing this control. First, how to get the user inputted text from the ASP.NET control to the Silverlight control? Turns out the solution is to build a JavaScript to Silverlight bridge. The second challenge was coming up with the algorithm to parse out the formatting tags as the user enters them. This wasn't a big deal for a single set of tags, but there's some special cases, including handling embedded tags (where the user might want text that is both bolded and italicized, for example), handling "stranded" opening or closing tags, and peculiarities such as when you have something like:

					   1: [u][b]here is some formatted text[/u][/b]
	

where the opening and closing tags are entered out of order. I decided to support this scenario to enhance the customer experience. Of course, the parsing itself screams for recursion; that was how I did it.

Full source code is available at the bottom of this post.

The ASP.NET Hosting Page

Of course the control eventually made its way to my web site, but for testing I used the basic ASP.NET page set up when I created my new Silverlight project. That file is called LiveCommentPreviewTestPage.aspx. In it, you'll find the usual Silverlight reference at the top:

   1: <%@ Register Assembly="System.Web.Silverlight" Namespace="System.Web.UI.SilverlightControls" TagPrefix="asp" %>

and the declaration of the Silverlight control itself:

   1: <div style="height:100%;">
   2:    <asp:Silverlight ID="slLivePreview" runat="server" Source="~/ClientBin/ItsCodingTime.LiveCommentPreview.xap" MinimumVersion="2.0.31005.0" Width="100%" Height="100%" />
   3: </div>

The piece that carries the data from ASP.NET/JavaScript to the Silverlight control is this:

   1: <script type="text/javascript">
   2:  
   3: function UpdateLivePreview ()
   4: {
   5:     var sl = document.getElementById ("slLivePreview")        // get silverlight app
   6:     var str = document.getElementById ("TextBox1").value;    // get current text from textbox
   7:  
   8:     sl.content.JStoSLBridge.UpdateDisplay(str);
   9: }
  10:  
  11: </script>

I covered this bridging technique in a previous post, so I'll leave it to the reader to investigate this last aspect further.

Update: Just a note to help you avoid spending half a day wondering why your getElementById function call is not working when you know it should… Because of ASP.NET master page control id mangling, you might want to use:

   1: function UpdateLivePreview()
   2: {
   3:     var sl = document.getElementById('<%=slLivePreview.ClientID%>')       // get silverlight app
   4:     var str = document.getElementById('<%=TextBox1.ClientID%>').value;    // get current text from textbox
   5:  
   6:     sl.content.JStoSLBridge.UpdateDisplay(str);
   7: }

The 'ClientId' property will remain unmangled and match the Id you specified when you defined the control.

Page.xaml

The XAML is pretty straightforward:

   1: <UserControl x:Class="ItsCodingTime.LiveCommentPreview.Page"
   2:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
   3:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
   4:     <Grid x:Name="LayoutRoot" Background="White">
   5:         <Grid.RowDefinitions>
   6:             <RowDefinition></RowDefinition>
   7:         </Grid.RowDefinitions>
   8:  
   9:         <Border Background="BlanchedAlmond" CornerRadius="0" BorderBrush="Azure" BorderThickness="1">
  10:             <ScrollViewer x:Name="textScrollView" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" VerticalScrollBarVisibility="Auto">
  11:                 <TextBlock x:Name="uxLivePreview" TextWrapping="Wrap" Grid.Row="0" FontSize="14"></TextBlock>
  12:             </ScrollViewer>
  13:         </Border>
  14:     </Grid>
  15: </UserControl>

We have a Grid control that contains a Border, the TextBlock used to display the preview text, and a ScrollViewer so that if the text creeps beyond the view we can scroll down. We wrap text, so horizontal scrolling is not necessary.

Adding a "Run" to the TextBlock control

You add formatted (or unformatted) text to a TextBlock with a Run object. Properties on the Run define the extent of the formatting. For example, here I allocate a new Run object defining the text to display, a font weight of "normal", font style of "normal", and use "Arial" as the font:

   1: var newRun = new Run
   2: {
   3:     Text = textForRun,
   4:     FontWeight = FontWeights.Normal,
   5:     FontStyle = FontStyles.Normal,
   6:     FontFamily = new FontFamily ("Arial")
   7: };

You can then add this Run object to the TextBlock like this:

   1: uxLivePreview.Inlines.Add (newRun);

where uxLivePreview is my TextBlock and Inlines is an InlineCollection of Run's belonging to that TextBlock control.

Of course, the above is going to add unformatted text to the TextBlock. Actual formatting options that we're concerned with here are FontWeights.Bold for bolding text and FontStyles.Italic for italicizing text. Also, I use two fonts: Arial for normal text and Courier New for quoted text. For underlining, do this:

   1: newRun.TextDecorations = TextDecorations.Underline;

These formatting characteristics (all but underline, which is represented as a bool class member) are defined at class-scope as:

   1: private FontWeight _fontWeight = FontWeights.Normal;
   2: private FontStyle _fontStyle = FontStyles.Normal;
   3: private readonly FontFamily _fontFamilyNormal = new FontFamily ("Arial");
   4: private readonly FontFamily _fontFamilyQuote = new FontFamily ("Courier New");

This way I use what's there as the defaults for unformatted text. As I'm parsing along, when I come upon an open/close tag, I modify the above members based on the type of tag. This approach comes in handy as I encounter nested tags where you need to apply more than one type of formatting to a block of text. As I encounter each closing tag, I then revert the particular style back to it's "normal" setting and move along.

Page.xaml.cs

The Page.xaml.cs file is where all the real work is done. There are a lot of utility methods in there. I won't spend any time on those since it's pretty obvious what each one does. I would like to spend a little time discussing the more significant methods, however.

UpdateDisplay

   1: [ScriptableMember]
   2: public void UpdateDisplay (string commentText)
   3: {
   4:     // if the text hasn't changed, we're done
   5:     if (commentText == _previousText)
   6:     {
   7:         return;
   8:     }
   9:  
  10:     _previousText = commentText;
  11:  
  12:     uxLivePreview.Inlines.Clear ();    // let's text start with a clean slate
  13:     _textBlockRuns.Clear ();
  14:  
  15: #if DEBUG
  16:     _completeText = commentText;
  17:     Debug.WriteLine ("Complete text to parse [{0}]", _completeText);
  18: #endif
  19:  
  20:     // scan input text for [b][/b], [i][/i], [u][/u], [q][/q] and replace with appropriate formatting
  21:  
  22:     var text = commentText;
  23:     ParseText (ref text);
  24:     if (!string.IsNullOrEmpty (text))
  25:     {
  26:         AddNewRun (text);    // add any remaining text
  27:     }
  28:  
  29:     foreach (var r in _textBlockRuns)
  30:     {
  31:         uxLivePreview.Inlines.Add (r);
  32:     }
  33:  
  34:     return;
  35: }

UpdateDisplay is exposed to JavaScript using the ScriptableMember attribute and is initiated from our aspx web page.

The first thing I do (lines 5-8) is examine the incoming text to see if it's changed from the previously saved text (line 10). If it hasn't, we're done. If it has changed, we keep going.

Next, I pave the way for new text content by clearing out the current content of the control as well as the collection I use to store Run objects (lines 12-13).

In debug code, I save and output the incoming text (lines 15-18).

Now, the good stuff. ParseText is where we dissect the incoming text, examine any formatting tags in place, create the formatted Run's, and add them to our Runs collection (_textBlockRuns). ParseText does this in recursive fashion. See below for a further breakdown of ParseText.

Last, after ParseText has added all of the Run's to _textBlockRuns, we iterate through the collection and add each Run to the TextBlock control (lines 29-32).

ParseText

   1: public void ParseText (ref string text)
   2: {
   3:     if (string.IsNullOrEmpty (text))
   4:     {
   5:         return;
   6:     }
   7:  
   8:     if ((!ContainsOpenTag (text)) && (!ContainsCloseTag (text)))
   9:     {
  10:         // if there's no formatting, then just return the whole string as the only TextBlock Run
  11:         AddNewRun (text);
  12:         text = string.Empty;
  13:         return;
  14:     }
  15:  
  16:     if ((ContainsOpenTag (text)) && (!ContainsCloseTag (text)))
  17:     {
  18:         AddNewRun (text);
  19:         text = string.Empty;
  20:         return;
  21:     }
  22:  
  23:     int closeTagPosition;
  24:  
  25:     if ((!ContainsOpenTag (text)) && (ContainsCloseTag (text)))
  26:     {
  27:         closeTagPosition = FindFirstCloseTagPosition (text);
  28:  
  29:         var ft = GetFirstCloseTagType (text);
  30:  
  31:         if (((ft == FormattingType.Underline) && (_underlinePass))
  32:             || ((ft == FormattingType.Quote) && (_quotePass))
  33:             || ((ft == FormattingType.Bold) && (_fontWeight != FontWeights.Normal))
  34:             || ((ft == FormattingType.Italics) && (_fontStyle != FontStyles.Normal))
  35:         )
  36:         {
  37:             AddNewRun (text.Substring (0, closeTagPosition));
  38:         }
  39:         else
  40:         {
  41:             AddNewRun (text.Substring (0, closeTagPosition + 4));    // corresponding open tag not being processed, so output closing tag
  42:         }
  43:  
  44:         text = text.Substring (closeTagPosition + 4);
  45:  
  46:         return;
  47:     }
  48:  
  49:     // find any untagged text from the start of the string to the first open tag
  50:     var openTagPosition = FindFirstOpenTagPosition (text);
  51:     if (openTagPosition > 0)
  52:     {
  53:         AddNewRun (text.Substring (0, openTagPosition));
  54:         text = text.Substring (openTagPosition);
  55:     }
  56:  
  57:     openTagPosition = 0;
  58:  
  59:     var openTag = text.Substring (0, 3);    // should be guaranteed to always have an opening tag as the first characters
  60:  
  61:     if (openTag == _openTagCollection[(int) FormattingType.Bold])
  62:     {
  63:         var foundClosingTag = true;    // assume there is a corresponding closing tag for now
  64:  
  65:         closeTagPosition = text.IndexOf (_closeTagCollection[(int) FormattingType.Bold], openTagPosition + 3); // find the corresponding closing tag
  66:  
  67:         // only display with formatting if there is a corresponding closing tag
  68:         if (closeTagPosition != -1)
  69:         {
  70:             _fontWeight = FontWeights.Bold;
  71:         }
  72:  
  73:         if (closeTagPosition == -1)
  74:         {
  75:             foundClosingTag = false;
  76:  
  77:             // use end of string as closing tag location if we can't find one
  78:             closeTagPosition = text.Length;
  79:         }
  80:  
  81:         // look for nested open tag
  82:         if (ContainsOpenTag (text.Substring (openTagPosition + 3, closeTagPosition - openTagPosition - 3)))
  83:         {
  84:             if (foundClosingTag)
  85:             {
  86:                 text = text.Substring (openTagPosition + 3);
  87:             }
  88:             else
  89:             {
  90:                 // get the position of the nested open tag
  91:                 var nestedOpenTagPosition = FindFirstOpenTagPosition (text.Substring (openTagPosition + 3));
  92:                 AddNewRun (text.Substring (openTagPosition, nestedOpenTagPosition - openTagPosition + 3));    // add the section up to the nested open tag
  93:                 text = text.Substring (nestedOpenTagPosition + 3);
  94:             }
  95:  
  96:             ParseText (ref text);
  97:         }
  98:  
  99:         if ((closeTagPosition = text.IndexOf (_closeTagCollection[(int) FormattingType.Bold], openTagPosition)) != -1)
 100:         {
 101:             text = GenerateAndAddNewRun (text, openTagPosition, closeTagPosition, _openTagCollection[(int) FormattingType.Bold]);
 102:             _fontWeight = FontWeights.Normal;
 103:             ParseText (ref text);
 104:         }
 105:  
 106:         _fontWeight = FontWeights.Normal;
 107:     }
 108:     else if (openTag == _openTagCollection[(int) FormattingType.Italics])
 109:     {
 110:         var foundClosingTag = true;    // assume there is a corresponding closing tag for now
 111:  
 112:         closeTagPosition = text.IndexOf (_closeTagCollection[(int) FormattingType.Italics], openTagPosition + 3); // find the corresponding closing tag
 113:  
 114:         // only display with formatting if there is a corresponding closing tag
 115:         if (closeTagPosition != -1)
 116:         {
 117:             _fontStyle = FontStyles.Italic;
 118:         }
 119:  
 120:         if (closeTagPosition == -1)
 121:         {
 122:             foundClosingTag = false;
 123:  
 124:             // use end of string as closing tag location if we can't find one
 125:             closeTagPosition = text.Length;
 126:         }
 127:  
 128:         // look for nested open tag
 129:         if (ContainsOpenTag (text.Substring (openTagPosition + 3, closeTagPosition - openTagPosition - 3)))
 130:         {
 131:             if (foundClosingTag)
 132:             {
 133:                 text = text.Substring (openTagPosition + 3);
 134:             }
 135:             else
 136:             {
 137:                 // get the position of the nested open tag
 138:                 var nestedOpenTagPosition = FindFirstOpenTagPosition (text.Substring (openTagPosition + 3));
 139:                 AddNewRun (text.Substring (openTagPosition, nestedOpenTagPosition - openTagPosition + 3));    // add the section up to the nested open tag
 140:                 text = text.Substring (nestedOpenTagPosition + 3);
 141:             }
 142:  
 143:             ParseText (ref text);
 144:         }
 145:  
 146:         if ((closeTagPosition = text.IndexOf (_closeTagCollection[(int) FormattingType.Italics], openTagPosition)) != -1)
 147:         {
 148:             text = GenerateAndAddNewRun (text, openTagPosition, closeTagPosition, _openTagCollection[(int) FormattingType.Italics]);
 149:             _fontStyle = FontStyles.Normal;
 150:             ParseText (ref text);
 151:         }
 152:  
 153:         _fontStyle = FontStyles.Normal;
 154:     }
 155:     else if (openTag == _openTagCollection[(int) FormattingType.Underline])
 156:     {
 157:         var foundClosingTag = true;    // assume there is a corresponding closing tag for now
 158:  
 159:         closeTagPosition = text.IndexOf (_closeTagCollection[(int) FormattingType.Underline], openTagPosition + 3); // find the corresponding closing tag
 160:  
 161:         // only display with formatting if there is a corresponding closing tag
 162:         if (closeTagPosition != -1)
 163:         {
 164:             _underlinePass = true;
 165:         }
 166:  
 167:         if (closeTagPosition == -1)
 168:         {
 169:             foundClosingTag = false;
 170:  
 171:             // use end of string as closing tag location if we can't find one
 172:             closeTagPosition = text.Length;
 173:         }
 174:  
 175:         // look for nested open tag
 176:         if (ContainsOpenTag (text.Substring (openTagPosition + 3, closeTagPosition - openTagPosition - 3)))
 177:         {
 178:             if (foundClosingTag)
 179:             {
 180:                 text = text.Substring (openTagPosition + 3);
 181:             }
 182:             else
 183:             {
 184:                 // get the position of the nested open tag
 185:                 var nestedOpenTagPosition = FindFirstOpenTagPosition (text.Substring (openTagPosition + 3));
 186:                 AddNewRun (text.Substring (openTagPosition, nestedOpenTagPosition - openTagPosition + 3));    // add the section up to the nested open tag
 187:                 text = text.Substring (nestedOpenTagPosition + 3);
 188:             }
 189:  
 190:             ParseText (ref text);
 191:         }
 192:  
 193:         if ((closeTagPosition = text.IndexOf (_closeTagCollection[(int) FormattingType.Underline], openTagPosition)) != -1)
 194:         {
 195:             text = GenerateAndAddNewRun (text, openTagPosition, closeTagPosition, _openTagCollection[(int) FormattingType.Underline]);
 196:             _underlinePass = false;
 197:             ParseText (ref text);
 198:         }
 199:  
 200:         _underlinePass = false;
 201:     }
 202:     else if (openTag == _openTagCollection[(int) FormattingType.Quote])
 203:     {
 204:         var foundClosingTag = true;    // assume there is a corresponding closing tag for now
 205:  
 206:         closeTagPosition = text.IndexOf (_closeTagCollection[(int) FormattingType.Quote], openTagPosition + 3); // find the corresponding closing tag
 207:  
 208:         // only display with formatting if there is a corresponding closing tag
 209:         if (closeTagPosition != -1)
 210:         {
 211:             _quotePass = true;
 212:         }
 213:  
 214:         if (closeTagPosition == -1)
 215:         {
 216:             foundClosingTag = false;
 217:  
 218:             // use end of string as closing tag location if we can't find one
 219:             closeTagPosition = text.Length;
 220:         }
 221:  
 222:         // look for nested open tag
 223:         if (ContainsOpenTag (text.Substring (openTagPosition + 3, closeTagPosition - openTagPosition - 3)))
 224:         {
 225:             if (foundClosingTag)
 226:             {
 227:                 text = text.Substring (openTagPosition + 3);
 228:             }
 229:             else
 230:             {
 231:                 // get the position of the nested open tag
 232:                 var nestedOpenTagPosition = FindFirstOpenTagPosition (text.Substring (openTagPosition + 3));
 233:                 AddNewRun (text.Substring (openTagPosition, nestedOpenTagPosition - openTagPosition + 3));    // add the section up to the nested open tag
 234:                 text = text.Substring (nestedOpenTagPosition + 3);
 235:             }
 236:  
 237:             ParseText (ref text);
 238:         }
 239:  
 240:         if ((closeTagPosition = text.IndexOf (_closeTagCollection[(int) FormattingType.Quote], openTagPosition)) != -1)
 241:         {
 242:             text = GenerateAndAddNewRun (text, openTagPosition, closeTagPosition, _openTagCollection[(int) FormattingType.Quote]);
 243:             _quotePass = false;
 244:             ParseText (ref text);
 245:         }
 246:  
 247:         _quotePass = false;
 248:     }
 249:  
 250:     return;
 251: }

ParseText is a recursive method that examines input text for open/close formatting tags and add new Run objects to a List collection based on those formatting tags, if any.

The method itself is rather long (about 250 lines), and I won't go through every bit of it. There is a fair amount of repetition in the general manner in which tags are searched for and acted upon. The difference really comes in which tag we're actively working on and what formatting needs to be applied based on that.

So, to break it down just a bit, we do some basic checking right at the beginning: if the string is null or empty (lines 3-6), there's not much we can do. Also, if the text contains no formatting tags, we add the whole thing as unformatted (lines 8-14). Easy, we're done. Another one is if there's only open tags but no closing tags. Last, because we're recursive, we have to consider that we're already working on an open tag, so we look for the case where the text only contains closing tags but no open tags (starting at line 25). From there, we look to see if we are in fact already acting on an open tag and add the text accordingly.

You'll notice that I have two bool's (_underlinePass, _quotePass) and two font attributes (_fontWeight, _fontStyle) that I use as flags to keep track of what formatting I'm currently working on.

Once we're through that, I look for the first open tag position (line 50). If it's not at position 0, then I add up to openTagPosition as an unformatted Run. The rest of the text requires further examination since we are now operating on an open tag.

I get the actual open tag (line 59), then determine which one it is. Let's assume it's a [b] tag and go from there. We enter the section of code begun on line 63 by first finding the first, corresponding close tag that appears after our open tag (line 65). We only display the text as bolded if we find a matching closing tag (lines 68-71). If we didn't find a matching close tag, we do some bookkeeping (lines 73-79). Then it's time to look for nested open tags where a user might want to apply more than one formatting attribute to some text (lines 82-97). Based on what we find, we break out that section of text and make a recursive call back into ParseText to handle just that bit of text. Once we're done with that, we check for a bold closing tag. It's possible to be left with something like "and here we have some bolded text[/b]", where we already acted on everything up to "and…" but just need to finish it up now (lines 99-104). Last, we reset the _fontWeight flag to Normal (line 106). We're done processing that bold run of text.

You'll notice that the recursion works by breaking off a section of text up to a certain point based on whether we find a nested open tag, a closing tag, or something else. It then processes each segment in a sort of serial fashion, using the four flags mentioned above to know what formatting is currently being processed. This seemed to be the most straightforward and intuitive way of processing runs of text from the main user input text.

The other blocks of code to handle each formatting type act pretty much the same. In fact, there's probably some room for simplification there since a good chunk of the code is duplicated, differing only by the type of tag we're acting on. I might come back and revisit at some point, but not now. ;-)

Conclusion

I found this a good lesson in how to effectively communicate information from an ASP.NET page to a Silverlight control while also re-enabling the live comment preview feature of my blog. Feel free to tweak or use as-is the code below.

Download: LiveCommentPreview.zip

References

The TextBlock Inline property in Silverlight 2