65.9K
CodeProject 正在变化。 阅读更多。
Home

在 Silverlight RichTextBox 中创建 Actionlinks

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (5投票s)

2010年3月15日

CPOL

6分钟阅读

viewsIcon

52676

downloadIcon

929

扩展 Silverlight RichTextBox,使其支持交互式文本

引言

这篇文章的标题也可以是“告别 Hyperlink,迎接 Actionlink”或“在 Silverlight 中创建交互式文本”。但唉,只能有一个。

超链接非常有用。但是,它们也有局限性,因为它们的动作是固定的:浏览到 URL。这对于互联网的初期来说可能已经足够了,但如今,在 Web 应用程序中,我们希望发生的事情是完全改变上下文。在应用程序中,我们通常希望超链接的选择能够触发一个动作,从而更新屏幕的一部分。例如,如果我的应用程序显示了一个地图,旁边有一些文本,地图会响应文本中超链接的选择,例如通过放大某个位置并在弹出窗口中显示额外的地理位置信息。这样,文本就变成了交互式文本

要求

一家公司为许多客户公司创建和维护网站是很常见的。为了降低维护成本,重要的是这些网站的内容能够由客户公司自己更新,而无需软件工程师的参与。为了适应这种情况,我们希望交互式文本的作者能够配置所有超链接(而无需编写任何代码)。

在 Silverlight 的 RichTextBox 中,Hyperlink 的默认操作与传统的超链接相同,但它可以被改变:如果 Command 属性有一个值,那么在点击事件发生时,这个命令就会被调用,并且将 CommandParameter 的值作为参数。我们如何让文本的作者为文本中的每个超链接指定一个命令,以及如何让应用程序正确响应超链接选择事件?

我们这里谈论的是任何命令。显然,应用程序只会识别一组特定的命令,并带有明确定义的参数,但我们这里采取的方法是通用的,因为它与 RichTextBox 和任何命令有关。

那么我们需要什么?我们希望

  1. 作为一名文本作者,我可以在(富)文本中配置超链接的动作,而无需编写代码
  2. 作为一名文本作者,我可以将超链接的动作与文本一起持久化
  3. 作为持久化文本的读者,我可以点击超链接,并且已配置的动作将会发生
  4. 作为一名应用程序开发者,我可以配置一个控件来使用我应用程序特定的命令

实现

John Papa 关于 RichTextBox 的精彩介绍中,他展示了(除其他内容外)如何持久化使用此控件创建的文本。为了满足我们的需求,我们可以创建一个 RichTextBox 的子类,该子类使用 John 的代码,并允许插入两个特定于命令的组件:一个用于提示命令定义,另一个用于执行命令。由于这两个插件都特定于应用程序,我们的 RichTextBox 子类不应假设它们除了接口之外的任何东西。

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

IDefineCommand 插件接收链接的内容(读者可见的文本)和(可选的)关联命令,并显示某种控件,允许作者定义链接(包括文本和命令)。完成后,这个(可能已更改的)内容字符串将与一个定义当读者点击已发布文本中的链接时要执行的命令的对象一起传回 RichTextBox
IPerformCommand 插件只是实现了 System.Windows.Input.ICommand

让我们使用MEF 来加载合适的插件。在示例解决方案中,有一个项目包含这些插件的基本实现。IDefineCommand 插件仅提示输入一个命令字符串(参见命令行或查询字符串),而 IPerformCommand 插件显示一个 MessageBox 来展示这个命令字符串。一个使用这个扩展的 RichTextBox 的实际应用程序将拥有自己的一套命令,每个命令都有自己的参数,因此将提供更用户友好的特定于应用程序的插件。
尽管如此,在任何情况下,命令都可以作为 string 持久化,因此上述两个接口就足够了。

John Papa 的解决方案被用作起点。

步骤

项目 InteractiveText

创建一个名为 InteractiveTextBox 的新的 Silverlight 类库项目。将 Class1.cs 文件重命名为 InteractiveTextBox.cs,让 InteractiveTextBox 派生自 RichTextBox。在 RichTextBox 项目中,添加对 InteractiveText 项目的引用,并在 MainPage.xaml 中用 InteractiveTextBox 替换 RichTextBox。请注意,右键单击功能区不会显示 ContextMenu (MPContextMenu),但代码显示这是预期的。一种使其生效的方法是将 mainPanel_MouseRightButtonUp/Down 附加到“mainPanelRectangle 之后的 Grid。在 MPContextMenu.cs 文件中,确保(在第 100 行)选择(单个)菜单项实际上会切换编辑/查看模式。

mp.rtb.ToggleReadOnly(); 

项目 ActionLink.Contracts

向解决方案中添加另一个名为 ActionLink.Contracts 的 Silverlight 类库项目。该项目仅定义两个接口,用于定义命令和执行命令,如上所述。

项目 ActionLink.Implementation

向解决方案中添加另一个名为 ActionLink.Implementation 的 Silverlight 类库项目。该项目实现了 ActionLink.Contracts 项目中定义的两个接口。在示例代码中,实现 IDefineCommand 的类仅提示输入一个 string。这个项目通常需要被特定于应用程序的实现所替换;IDefineCommand 类将具有特定于应用程序的用户界面,并将用户输入“序列化”为一个 string,例如“type=5;par=3;”以在文本中持久化。

ActionLink.Implementation 项目应引用 ActionLink.Contracts 项目,InteractiveText 项目也应如此。我们的 InteractiveTextBox 使用 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);
    }
}

为了确保 XAP 文件中有一个实现 IDefineCommand 的类,请在主 RichTextBox 项目(我们解决方案中的原始 Silverlight 项目)中添加对 ActionLink.Implementation 项目的引用。现在我们可以运行应用程序,但暂时看不到任何新内容。

定义命令

我们尽量少做,替换 btnHyperlink_Click 方法的实现(毕竟,超链接只是 ActionLink 的一个特例)。也就是说,我们不创建 InsertURL 子窗口,而是使用实现 IDefineCommand 的类:在我们的应用程序(MainPage)中

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

在我们的 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);
}

显然,如果一个超链接已经有了命令定义,我们希望在支持命令定义的控件中显示它。不幸的是,鉴于 RichTextBox 当前的设计方式,这很难做到。如果我们选择文本中的超链接,我们就无法访问与之对应的 Hyperlink 对象;RichTextBox 的 Selection 只提供对 XAML 的访问。如果 Hyperlink 的 CommandParameter 属性被持久化在 XAML 中,这将足够了,但事实并非如此。这个疏忽迫使我们进入“无论如何都要让它工作”的模式:将其放入 NavigateUri(ugh),并在加载 XAML 时修复 CommandParameter

执行命令

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);
                }
            }
        }
    }
}

测试

为了测试这一点,请选择一段文本,然后单击超链接图标。这将导致实现 IDefineCommand 的对象提示输入命令。输入一个命令字符串。

然后通过右键单击功能区来更改 RichTextBox 的模式。在此之后,单击 Hyperlink 以查看您输入的命令字符串。

历史

  • 2010 年 3 月 14 日 - 第一个版本(使用 RichTextArea
  • 2010 年 3 月 28 日 - 更新为使用最新的 Silverlight 4 和 RichTextBox
© . All rights reserved.