Skip to main content Skip to footer

How to Implement a Smart Tag System using C1TextParser

We all need to deal with a lot of data in text form every day. Based on the data we read, we decide to take appropriate action. However, only a part of the data carries the information that is useful to us. In search of this small subset of information, we become “human parsers”. What if we could delegate this task to an actual parser?

In this post, we are going to see how we can implement a SmartTag system using one of our Service library: C1TextParser. Smart Tag is a feature initially introduced in Microsoft Word which recognizes parts of text, highlights them in some form, and optionally adds an action which can be performed using that part of the text.

Some of the things which can be augmented using a Smart Tag include a phone number, tracking number, an email, a contact name, etc. By having an application support SmartTags, it allows the end-user to have a feature-rich document that provides the ability to interact with their documents in a way which would be impossible to do with a plain text document.

Download Now!

You can see how useful this can be from the following demo:

demo

By adding a few SmartTags, we're able to:

  • See useful information from the document at a glance (the stock ticker, which stocks closed up/down)
  • View a summary of a particular stock from the Tooltip
  • View price trend for a particular stock by clicking on its symbol
  • Add the symbol for a new Stock and immediately do all the things we could do with existing parts of the document
  • Compare the Price trend for 2 (or more) Stocks
  • Edit a date text anywhere in the document using DatePicker control

So, we have seen what SmartTags can do for us. Let's see how we can implement a SmartTag system using C1TextParser library.

Before we start, we need to decide how and where we are going to have the text information. Since C1TextParser only performs operations on a text stream, we’ll need a document and the ability to manipulate parts of the document visually. C1RichTextBox is perfect for this since it provides APIs to edit and manipulate parts of the document.

Now that we have decided what we are going to use, here’s what we’ll do:

  • Create a simple WPF application with C1RichTextBox
  • Create a Parser using C1TextParser to create Smart Tags
  • Format the created Smart Tags using C1RichTextBox
  • Associate an action with the Smart Tag

Create a WPF Application with C1RichTextBox

We first create a WPF application and add C1RichTextBox in MainWindow.xaml:

c1

We also add a simple HTML document which will be loaded in C1RichTextBox:

MainWindow.xaml.cs
c1RichTextBox.Html = System.IO.File.ReadAllText("Data\\document.html");
document.html
<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body style="font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-size: 1.5em;">
    <h2>Stock Portfolio</h2>
    <ul>
        <li>Apple (AAPL)  +2.34%</li>
        <li>Bank of America (BAC)  -3.25%</li>
        <li>Coca-Cola (KO)  +0.47%</li>
        <li>Wells Fargo (WFC)  +0.20%</li>
        <li>American Express (AXP)  -0.8%</li>
    </ul>
</body>
</html>

Next, we will make this simple document more interactive by using Smart Tags. For that we'll need a Parser to create these Smart Tags.

Create a Parser Using C1TextParser to Create Smart Tags

Configure C1TextParser

First, we’ll need to install the Nuget package for C1TextParser. We can do so by using the Nuget Package Manager:

text

Since we have created a WPF application, we install C1.WPF.TextParser.

Create a Smart Tag Template Document

We want our Smart Tag system to be easily modifiable so we are going to create a template for this purpose. C1TextParser’s Template based Extractor is ideal for this purpose, however, we don’t just want to create one Smart Tag, we also want it to be extensible.

We should create a custom template structure which can have the actual parsing template embedded in it.

smart-tag-template.xml
<SmartTags>
  <SmartTag name="StockTicker">
    <ParserTemplate>
      <template name="StockTicker" extractFormat="regex:[A-Z]{1,4}" />
    </ParserTemplate>
    <Styles>
      <Style name="BorderBrush" value="#FF008080" />
      <Style name="BorderThickness" value="0,0,0,3" />
      <Style name="Background" value="#FFEEE8AA" />
    </Styles>
  </SmartTag>
</SmartTags>

Here, we’ve added two elements for a Smart Tag:

  • ParserTemplate – Parsing template used by C1TextParser
  • Styles – Styling properties used by C1RichTextBox to highlight the Smart Tag

Create a Utility to Read a Smart Tag Template

Since we created a custom XML document to store configuration for the Smart Tags, we also need something to read this document.

For this purpose, we create a class named TemplateReader:

TemplateReader.cs
public class TemplateReader
{
    private XmlDocument document;

    public TemplateReader(string filepath)
    {
        document = new XmlDocument();
        document.Load(filepath);
    }
}

We also add a few methods in TemplateReader to get different elements from our Smart Tag template:

TemplateReader.cs
// Gets the names of all Smart Tags
public IEnumerable<string> GetSmartTags()
{
    var nodes = document.SelectSingleNode("/SmartTags").ChildNodes;
    foreach (XmlNode node in nodes)
    {
        yield return node.Attributes["name"].Value;
    }
}

// Gets the Parsing template for the given Smart Tag
public string GetSmartTagTemplate(string name)
{
    XmlNode node = document.SelectSingleNode($"/SmartTags/SmartTag[@name='{name}']");
    XmlNode templateNode = node.SelectSingleNode("ParserTemplate");

    return templateNode.InnerXml;
}

The Styles element is a little different. We will eventually use StyleOverrides of C1RichTextBox to highlight the SmartTags.

We need to create an object of C1TextElementStyle from the Style nodes at runtime. We can do this using reflection:

TemplateReader.cs
public C1TextElementStyle GetSmartTagStyle(string name)
{
    XmlNode node = document.SelectSingleNode($"/SmartTags/SmartTag[@name='{name}']");
    XmlNode styles = node.SelectSingleNode("Styles");

    C1TextElementStyle style = new C1TextElementStyle();
    foreach (XmlNode styleNode in styles.ChildNodes)
    {
        if (styleNode.NodeType != XmlNodeType.Element)
            continue;

        string styleName = styleNode.Attributes["name"].Value;
        var propertyInfo = typeof(C1TextElement).GetProperties(BindingFlags.Instance | BindingFlags.Public).
             Where(prop => prop.Name == styleName).FirstOrDefault();

        StyleProperty styleProperty = (StyleProperty)typeof(C1TextElement).GetFields().
             Where(field => field.Name == styleName + "Property").FirstOrDefault().GetValue(null);

        string styleValue = styleNode.Attributes["value"].Value;

        Type propType = propertyInfo.PropertyType;
        if (propType == typeof(Brush))
        {
            style[styleProperty] = new SolidColorBrush((Color)ColorConverter.ConvertFromString(styleValue));
        }
        return style;
}

Above snippet shows how we can read the value for a Color. Other types of Style properties can be read similarly.

Create Parsers for SmartTags

We have seen how we can store Smart Tag configuration in an XML document and then read it. Now, let’s put it together to create Parsers for the Smart Tags.

Since using Parsers for each type of Smart Tag is quite cumbersome, we abstract this into a single Parser and let it handle all the details about the XML template and its reader.

SmartTagParsers.cs
public class SmartTagParsers
{
    private Dictionary<string, IExtractor> extractors;
    private TemplateReader templateReader;

    public SmartTagParsers()
    {
        extractors = new Dictionary<string, IExtractor>();
    }

    public void Add(string name, IExtractor extractor)
    {
       extractors.Add(name, extractor);
    }
}

Here, we created a class named SmartTagParsers which will have IExtractor objects for each type of Smart Tag. IExtractor is the interface used by C1TextParser to parse text, so we just map an IExtractor with a Smart Tag name using the Dictionary object extractors.

We also add a Parse method to our SmartTagParsers which will create a List of SmartTag objects that are found in given text. Since TemplateBasedExtractors parse the result into a JSON object we convert it to a simple SmartTag object.

SmartTag.cs
public class SmartTag
{
    public string Name { get; }
    public string Text { get; }

    public SmartTag(string name, string text)
    {
        Name = name;
        Text = text;
    }
}
SmartTagParsers.cs
public List<SmartTag> Parse(string text)
{
    List<SmartTag> smartTags = new List<SmartTag>();
    foreach (var entry in extractors)
    {
        using (var stream = text.ToStream())
        {
            var result = entry.Value.Extract(stream);
            string json = result.ToJsonString();
            JObject jObject = JsonConvert.DeserializeObject<JObject>(json);
            jObject = jObject.Value<JObject>("Result");
            JToken token = jObject.Value<JToken>(entry.Key);

            if (token?.Type == JTokenType.Array)
            {
                foreach (var childToken in token.Children())
                {
                    smartTags.Add(new SmartTag(entry.Key, childToken.ToString()));
                }
            }
        }
    }
    return smartTags;
}

Finally, we add a GetStyle method which just obtains the Style from the TemplateReader:

SmartTagParsers.cs
public C1TextElementStyle GetStyle(string name)
{
    return templateReader.GetSmartTagStyle(name);
}

Format the Created Smart Tags Using C1RichTextBox

We are now ready to use the Parser with C1RichTextBox. For this, we first create an object of C1RangeStyleCollection and add this in StyleOverrides of C1RichTextBox.

MainWindow.xaml.cs
public partial class MainWindow : Window
{
    private C1RangeStyleCollection rangeStyles = new C1RangeStyleCollection();
    private SmartTagParsers smartTagParsers;
    private List<SmartTag> smartTags;

    public MainWindow()
    {
        InitializeComponent();

        c1RichTextBox.StyleOverrides.Add(rangeStyles);

        smartTagParsers = new SmartTagParsers();
        smartTagParsers.LoadTemplate("Parser\\smart-tag-template.xml");

        c1RichTextBox.Html = System.IO.File.ReadAllText("Data\\document.html");
    }
}

We then add some methods to parse the document and fill this Style collection for each Smart Tag:

MainWindow.xaml.cs
private void AnnotateSmartTags(C1RichTextBox richTextBox)
{
    smartTags = smartTagParsers.Parse(richTextBox.Text);
    AddSmartTags(richTextBox, smartTags);
}

private void AddSmartTags(C1RichTextBox richTextBox, List<SmartTag> smartTags)
{
    rangeStyles.Clear();

    foreach (SmartTag smartTag in smartTags)
    {
        foreach (Match match in Regex.Matches(c1RichTextBox.Text, Regex.Escape(smartTag.Text)))
        {
            var textRange = richTextBox.GetTextRange(match.Index, match.Length);
            C1TextElementStyle style = smartTagParsers.GetStyle(smartTag.Name);                   

            rangeStyles.Add(new C1RangeStyle(textRange, style));
        }
    }
}

With these changes, the document looks like this:

stock

Now we can highlight some useful information in the document; however, it would be more helpful if we could do something with these tags. For this, we can associate an Action with a SmartTag.

Associating an Action with a SmartTag

To associate an Action with a Smart Tag we first identify which part of the document is clicked by the user, and if it is a Smart Tag then show some useful information.

We do this using a utility method that identifies the text portion (for a particular Run of the document) currently residing under a given point:

MainWindow.xaml.cs
private C1TextRange GetElementUnderPoint(C1Run c1Run, Point point)
{
    var pointer = c1RichTextBox.GetPositionFromPoint(point);
    string text = c1Run.Text;
    int start = pointer.Offset;
    int end = pointer.Offset;

    while (start > 0 && char.IsLetterOrDigit(text, start - 1))
    {
       start -= 1;
    }
    while (end < text.Length - 1 && char.IsLetterOrDigit(text, end + 1))
    {
        end += 1;
    }

    var word = new C1TextRange(pointer.Element, start, end - start + 1);
    return word;
}

After this, we add an event handler for C1RichTextBox’s ElementMouseLeftButtonUp event:

MainWindow.xaml.cs
private void C1RichTextBox_ElementMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
    bool isCtrlKeyDown = Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl);
    if (isCtrlKeyDown && sender is C1Run c1Run)
    {
        Point point = e.GetPosition(c1RichTextBox);
        var word = GetElementUnderPoint(c1Run, point);

        if (smartTags.Any(tag => tag.Text == word.Text))
        {
            word.Runs.First().Cursor = Cursors.Hand;
            StockInfo info = stockService.GetStockInfo(word.Text);

            if (info != null && info.Quotes.Count > 0)
            {
                var stockTrend = new StockTrend(info);
                stockTrend.Owner = this;
                stockTrend.Show();
            }
        }
    }
}

In the handler, we check if the Control key is pressed. If it is, we try to get stock data for the text portion that was clicked. We use a StockService class to get this data from a JSON file, which can be easily modified to use a real-time Stock API.

Once we have the necessary data, we show this in a window where the data is plotted using C1FinancialChart.

Here’s the XAML snippet for the StockTrend window we have used:

StockTrend.xaml
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <c1:C1FinancialChart
        x:Name="financialChart"
        ChartType="Candlestick"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch"
        ItemsSource="{Binding StockInfo.Quotes}"
        Grid.Row="0">
        <c1:FinancialSeries Binding="High,Low,Open,Close,Volume" BindingX="Date" SeriesName="{x:Null}" ChartType="Line">
        </c1:FinancialSeries>
        <c1:C1FinancialChart.Layers>
            <c1:C1LineMarker x:Name="marker" Lines="Both" Interaction="Move" Alignment="Auto" DragLines="True" PositionChanged="positionChanged"/>
        </c1:C1FinancialChart.Layers>
    </c1:C1FinancialChart>

</Grid>

We can interact with the created SmartTags and make it even more useful:

smart tag

Creating more SmartTags is now easy. We just need to do two things:

  • Add the regex and style information in the SmartTag template,
  • If required, add an Action for the SmartTag by using necessary events and properties of the editor.

To check the implementation for the demo shown in the beginning of this post, download the sample.

Download Now!


Jitender Yadav

Associate Software Engineer
comments powered by Disqus