Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Creating Actionlinks in a Silverlight RichTextBox

0.00/5 (No votes)
28 Mar 2010 1  
Extending the Silverlight RichTextBox so that it supports interactive text

Introduction

The title of this article could also have been "Move over Hyperlink, here comes Actionlink" or "Creating interactive text in Silverlight." But alas, there can be only one.

Hyperlinks are very useful. However, they are also limited because their action is fixed: browse to a URL. This may have been adequate at the start of the Internet, but nowadays, in web applications, the one thing we do not want to happen is a complete change of context. In applications, we typically like a hyperlink selection to initiate an action that updates a part of the screen. For instance, if my application has a map displayed with some text next to it, the map would react to a selection of a hyperlink in the text, e.g. by zooming in on a location and displaying additional locational information in a popup. In this way, the text becomes interactive text.

Requirements

It is quite common that one company creates and maintains websites for many client companies. To keep maintenance cost low, it is important that the content of these websites can be updated by the client companies themselves, without the need to involve a software engineer. To accommodate this scenario, we want the author of the interactive text to configure all hyperlinks (without writing any code).

In a Silverlight RichTextBox, the default action of a Hyperlink is the same as a traditional hyperlink, but it can be changed: if the Command property has a value then upon a click event this command is called with the value of the CommandParameter as parameter. How can we let the author of the text specify a command for each hyperlink in the text, and how can we let an application react properly to a hyperlink selection event?

We are talking about any command here. Obviously, the application would recognize only a specific set of commands, with well defined parameters, but the approach we take here is generic in the sense that it pertains to the RichTextBox and any command.

So what do we require? We wish that:

  1. As a text author, I can configure the action of a hyperlink in a (rich) text without writing code
  2. As a text author, I can persist the action of a hyperlink with the text
  3. As a reader of persisted text, I can click a hyperlink and the configured action will happen
  4. As an application developer, I can configure a control to use my application specific commands

Implementation

In an excellent introduction to the RichTextBox, John Papa shows (among other things) how to persist a text created using this control. To meet our requirements, we can create a subclass of RichTextBox that uses John's code and allows plugging in two command specific components: one to prompt for a command definition, and one to execute the command. Since both of these plugins are application specific, our RichTextBox subclass should not assume anything about them except their interface.

[InheritedExport]
public interface IDefineCommand
{
    void Prompt(string content, string commandParameter, Action callback);
}
    
[InheritedExport]
public interface IPerformCommand : ICommand {}        

The IDefineCommand plugin receives the content of the link (the text visible to the reader) and (optionally) an associated command, and displays some kind of control that allows the author to define the link (both text and command). When that's done, this (possibly changed) content string is conveyed back to the RichTextBox, together with an object that defines the command to execute when the link is clicked by the reader of the published text.
The IPerformCommand plugin simply implements System.Windows.Input.ICommand.

Let's use MEF to load the proper plugins. In the example solution, there is a project that contains rudimentary implementations of these. The IDefineCommand plugin simply prompts for a command string (cf. a command line or query string), and the IPerformCommand plugin displays a MessageBox showing this command string. An actual application using this extended RichTextBox would have its own set of commands, each having their own parameters, and hence would provide more user friendly application specific plugins.
Nonetheless, in any case a command can be persisted as a string and hence the two interfaces defined above suffice.

John Papa's solution was used as a starting point.

Steps

Project InteractiveText

Create a new Silverlight Class Library project named InteractiveTextBox. Rename the Class1.cs file InteractiveTextBox.cs and let the InteractiveTextBox derive from RichTextBox. In project RichTextBox, add a reference to project InteractiveText, and replace the RichTextBox in MainPage.xaml by an InteractiveTextBox. Notice that a right click on the ribbon does not make ContextMenu (MPContextMenu) display but that the code shows this was intended. One way to make it work is to attach mainPanel_MouseRightButtonUp/Down to the Grid defined after the Rectangle named "mainPanel." In the MPContextMenu.cs file, make sure (on line 100) that the selection of the (single) menu item actually toggles the edit/view mode:

mp.rtb.ToggleReadOnly(); 

Project ActionLink.Contracts

Add another Silverlight Class Library project called ActionLink.Contracts to the solution. This project simply defines two interfaces that are used to define a command and to perform a command, as defined above. 

Project ActionLink.Implementation

Add another Silverlight Class Library project called ActionLink.Implementation to the solution. This project implements the two interfaces defined in project ActionLink.Contracts. In the example code, the class that implements IDefineCommand simply prompts for a string. It is this project that typically needs to be replaced by an application specific implementation; the IDefineCommand class would have an application specific user interface and "serialize" the user input in a string, e.g. "type=5;par=3;" to be persisted in the text.

Project ActionLink.Implementation should have a reference to project ActionLink.Contracts and the same applies to project InteractiveText. Our InteractiveTextBox imports this functionality using MEF:

public InteractiveTextBox()
            : base()
{
    try
    {
        var catalog = new PackageCatalog();
        catalog.AddPackage(Package.Current);
        var container = new CompositionContainer(catalog);
        container.ComposeParts(this);
    }
    catch (Exception exc)
    {
        throw new Exception("The application is missing an 
	IDefineCommand and/or IPerformCommand component. 
	Make sure the application refers to projects that implement these. ", exc);
    }
}

To make sure there is a class that implements IDefineCommand in the XAP file, add a reference to the ActionLink.Implementation project in the main RichTextBox project (the original Silverlight project in our solution). Now we can run the application and see nothing new yet.

Defining the Command

Let's do as little as necessary and replace the implementation of the btnHyperlink_Click method (after all, a hyperlink is just a special case of an ActionLink). That is, instead of creating an InsertURL child window, we use the class that implements IDefineCommand: In our app (MainPage):

private void btnHyperlink_Click(object sender, RoutedEventArgs e)
{
    rtb.DisplayActionDefinition();
}

In our InteractiveTextBox:

public void DisplayActionDefinition()
{
    if (_defineCommand != null)
    {
        // try get current value of CommandParameter
        string commandParameter = String.Empty;
        XElement root = XElement.Parse(this.Selection.Xaml);
        XNamespace ns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation";
        var linkElement = root.Element(ns + "Paragraph").Element(ns + "Hyperlink");
        if (linkElement != null)
        {
            XAttribute attr = linkElement.Attribute("CommandParameter");
            if (attr != null)
            {
                commandParameter = attr.Value;
            }
            else // delete this once CommandParameter is supported in TextSelection.Xaml
            {
                XAttribute attr1 = linkElement.Attribute("NavigateUri"); // abuse alert
                if (attr1 != null)
                {
                    commandParameter = attr1.Value.Replace(PREFIX, String.Empty);
                }
            }
        }
        _defineCommand.Prompt(this.Selection.Text, 
		commandParameter, ConsumeLinkDefinition);
    }
}

private void ConsumeLinkDefinition(string content, object linkDefinition)
{
    Hyperlink hyperlink = new Hyperlink();

    hyperlink.Inlines.Add(content);
    string def = Convert.ToString(linkDefinition);  // in our case we just have a string
    if (!String.IsNullOrEmpty(def))
    {
        hyperlink.CommandParameter = def;
    }

    rtb.Selection.Insert(hyperlink);
}

Obviously if a Hyperlink already has a command definition, we want to display it in the control that supports defining commands. Unfortunately this is hard to do given the current way RichTextBox is designed. If we select a hyperlink in the text, we do not get access to the Hyperlink object that corresponds with it; the RichTextBox Selection only gives access to the XAML. This would suffice if the CommandParameter property of the Hyperlink were persisted in the XAML, but it is not. This omission forces us in make-it-work-no-matter-what mode: put it in NavigateUri (Ugh) and upon loading the XAML, fix the CommandParameter.

Performing the Command

public new string Xaml
{
    get
    {
        return (this as RichTextBox).Xaml;
    }
    set
    {
        (this as RichTextBox).Xaml = value;
        if (_performCommand != null)
        {
            // MEF found the component implementing IPerformCommand
            SetCommands(Blocks);
        }
    }
}

private void SetCommands(BlockCollection blocks)
{
    var res = from block in blocks
              from inline in (block as Paragraph).Inlines
              where inline.GetType() == typeof(InlineUIContainer)
              select inline;

    foreach (var block in blocks)
    {
        Paragraph p = block as Paragraph;

        foreach (var inline in p.Inlines)
        {
            Hyperlink hlink = inline as Hyperlink;
            if (hlink != null)
            {
                string uri = hlink.NavigateUri.AbsoluteUri;
                if (uri.StartsWith(PREFIX))
                {
                    // We have a CommandParameter
                    hlink.Command = _performCommand;
                    // Delete the following once 
                    // CommandParameter is supported in TextSelection.Xaml
                    hlink.CommandParameter = uri.Substring(PREFIX.Length);
                }
            }
        }
    }
}

Test

To test this, select a piece of text and click the Hyperlink icon. This causes an object implementing IDefineCommand to prompt for a command. Enter a command string.

Then change the mode of RichTextBox by right-clicking on the ribbon. After this, click on the Hyperlink to see the command string you entered.

History

  • March 14, 2010 - First version (using RichTextArea)
  • March 28, 2010 - Updated for use of the newest Silverlight 4 with RichTextBox

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here