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:
- As a text author, I can configure the action of a hyperlink in a (rich) text without writing code
- As a text author, I can persist the action of a hyperlink with the text
- As a reader of persisted text, I can click a hyperlink and the configured action will happen
- 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)
{
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 {
XAttribute attr1 = linkElement.Attribute("NavigateUri"); 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); 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)
{
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))
{
hlink.Command = _performCommand;
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