Silverlight 2 Beta 2:第一个原生富文本编辑器






4.88/5 (17投票s)
与现有编辑器不同,我的RTE完全用C#编写,并且不使用任何HTML渲染或JavaScript。支持大多数常用功能!
1. 引言
现已支持Silverlight 2 Beta 2!
请访问www.codeplex.com进行讨论、错误报告、获取最新的错误修复和发布。一个名为“RichTextEdit beta 2.0”的稳定版本现已发布。我也更新了CodeProject上的文件!
现场演示可在此处获取。
整篇文章也可作为PDF文件在文档下载中找到。
许多人都在等待在Silverlight中输入富文本的方法。尽管我认为微软会推出自己的解决方案,但没有人知道要等多久以及会包含哪些功能。我的RTE提供了各种常用功能和详尽的文档。请注意,整个项目仍处于BETA阶段,在XP上存在一些奇怪的bug,在Vista上存在一些其他的bug。似乎这些bug并非由我的组件引起,因为Visual Studio会在那种情况下通知我。因此,在使用此组件的任何生产环境之前,请等待Silverlight 2的最终版本!当然,也可能有些功能仍未记录在案,或者已记录的功能行为不如预期。请随时报告此类不一致之处。
不完整的功能列表
- 在富文本框之间复制/粘贴格式化文本,以及从/向剪贴板复制/粘贴未格式化但启用了宏的文本。这意味着,在Windows剪贴板中,即使是表情符号等内容也会被保留。
- 您可以插入换行符、无序列表和块引用。
- 您可以使用各种键盘选择功能,如End/Home/PageUp/PageDown/Left/Up/Right/Down,Ctrl+A/End/Home,Ctrl+Shift+End/Home/Left/Right,“Shift”+“End/Home/PageUp/PageDown/Left/Up/Right/Down”等等……
- 支持通过“Ctrl”+“[数字小键盘]”直接输入Unicode字符。
- 支持所有Silverlight字体格式,甚至还有更多,如上标/下标格式。
- 您可以定义宏和一个合适的类来替换匹配的文本,例如表情符号……
- 与许多其他富文本编辑器不同,这个编辑器是完全实时的。这意味着不需要预览,因为编辑器允许直接编辑所有内容。
- 如果您只使用宏和
IRichTextObject
来扩展控件,您将自动获得所有控件元素安全内容序列化的支持。内容序列化还支持重新加载内容并再次编辑。 - 安全内容序列化消除了在服务器上存储用户键入的格式化文本并将其呈现给访问者时的任何潜在安全漏洞,因为它完全可验证。
- 您可以将字体格式限制为精心定义的自定义子集。这使您可以确保所有用户输入的文本都符合您的需求或网站设计(此功能目前尚未实现,仅为原型)。
- 快照功能提供了对格式化内容的便捷访问,还可以进行正则表达式的查找和替换等操作……
由于在文档下载中提供了完整的API文档,本节重点介绍组件。如果您真的想了解它,您必须阅读文档并使用演示。
1.1. 首次概览
上面的截图显示了一些格式设置和宏(如表情符号)的使用。它还使用了自定义设计的边框,您可以看到内部内容会自动重新对齐以适应外部边框。您还可以自定义选择的背景和前景颜色以及光标颜色。
显然有两个不常见的功能。第一是您可以将任何派生自FrameworkElement
的对象插入到文本流中。第二是这些对象(如宏)能够分配格式化信息,这将允许表情符号,例如,也像周围的文本一样被下划线和背景色化。
正如您所见,Silverlight的内插功能存在一个小问题。当字体背景与控件背景不一致时,它会在每个字符后产生可见的小分隔符。为了模糊化,这似乎是Vista的一个问题,因为在XP上,它的工作方式符合预期!
如果有人有解决此问题的办法,请告诉我!
下面是演示应用程序的压缩截图
1.2. 剪贴板支持
但它不止于此。如果您选择自定义对象,例如按钮,您将能够通过Ctrl+c/v或内部剪贴板方法复制/粘贴它,包括所有文本格式。如果您选择并复制宏到剪贴板,它们将被转换回文本表示形式,允许在应用程序边界之间复制/粘贴宏,而在同一应用程序中,格式化文本操作是有限制的。即使浏览器不支持剪贴板,或者用户已拒绝访问,也支持剪贴板操作。但那时,所有操作自然都限于当前应用程序。
这样,您就可以将自定义对象像字母一样处理,并可以使用退格键等常规文本删除/覆盖它们。
1.3. 未来功能
当然,这个组件并不完美。以下功能将在未来几个月内实现
- 一个XML脚本语言,用于在Expression Blend中初始化富文本内容。这也便于管理多种语言,因为内容和设计是分开的。
- 一种将文本环绕在控件周围的方法,就像Microsoft Office一样。目前,它们只是像字母一样插入,这限制了插入图像或任何其他富非文本内容的能力。但是,这个功能实现起来确实不容易,所以不要指望它。
- 需要
XamlWriter
的不可编辑XAML输出,而XamlWriter
目前尚不可用。 - 其他开发者可能提交的功能。
2. 演示之旅
为了理解如何使用这个组件,我强烈建议逐步学习演示文件。它利用了几乎所有功能,并展示了一些通过抽象RTE接口实现超链接或带正则表达式的查找/替换的技巧。以下章节引用“RichText\Demo\Page.xaml.cs”。
2.1. 初始化
由于RichtextEdit
派生自UserControl
,您可以像使用任何其他框架元素一样使用和初始化它。
RichEdit = new RichTextEdit();
RichEdit.AutoFocus = true;
RichEdit.InsertString(…);
RichEdit.OnSelectionChanged += new NotificationHandler(RichEdit_OnSelectionChanged);
RichEdit.OnContentChanged += new NotificationHandler(RichEdit_OnContentChanged);
MSNEmoticons.Apply(RichEdit);
RichEdit.RegisterObject(new SerializableButton(null));
上面的代码用测试字符串初始化内容,并使控件准备好接收两个特殊事件。这使得您的代码能够在选择或内容更改时收到通知。此外,应用了MSNEmoticons
扩展,它将用匹配的MSN图标替换常见的表情符号,如“:-)”。最后一行有些特别,稍后将介绍。除对象创建外的所有行都是可选的!
2.2. 更新工具栏
void RichEdit_OnSelectionChanged(object sender)
{
IsUpdating = true;
…
}
此事件将根据当前选择的格式更新整个演示GUI。此方法对于任何有用的工具栏-编辑器对都非常重要,您应该尝试理解其中的内容……最重要的是,所有格式属性,如RichTextEdit.FontAttributes
,都根据当前选择进行设置。这意味着,您只需在OnSelectionChanged
处理程序中读取它们并正确更新您的工具栏。
2.3. 查找和替换
要查找文本,我们需要利用RichTextEdit.Snapshot
。这允许我们直接操作字符串内容,这在处理富文本时并不常见。
private MatchCollection REGEX_Matches;
private Int32 REGEX_Index = 0;
private RichTextEdit.Snapshot REGEX_Snapshot;
private void BTN_Find_Click(object sender, RoutedEventArgs e)
{
REGEX_Snapshot = RichEdit.QueryText();
Regex Exp = new Regex(EDIT_Find.Text, RegexOptions.IgnoreCase |
RegexOptions.Multiline | RegexOptions.ECMAScript);
REGEX_Matches = Exp.Matches(REGEX_Snapshot.Text);
REGEX_Index = 0;
BTN_Replace.IsEnabled = true;
BTN_FindNext.IsEnabled = true;
BTN_FindNext_Click(null, null);
}
为了实现“查找下一个”方法,我们只需遍历所有匹配项……
private void BTN_FindNext_Click(object sender, RoutedEventArgs e)
{
if ((REGEX_Matches == null) || (REGEX_Matches.Count == 0))
{
BTN_FindNext.IsEnabled = false;
return;
}
if (REGEX_Matches.Count <= REGEX_Index)
REGEX_Index = 0;
// select match
Match m = REGEX_Matches[REGEX_Index++];
REGEX_Snapshot.Select(CursorPosition.End, m.Index, m.Length);
}
如您所见,快照还允许我们根据字符串偏移量选择富文本。
如果您还想替换富文本,这会变得稍微复杂一些。首先,我们需要删除当前匹配项所引用的文本。然后,我们插入替换项并选择它。
private void BTN_Replace_Click(object sender, RoutedEventArgs e)
{
if ((REGEX_Matches == null) || (REGEX_Matches.Count == 0))
return;
if (REGEX_Matches.Count <= REGEX_Index)
REGEX_Index = 1;
// replace selection
Match m = REGEX_Matches[REGEX_Index - 1];
Int32 iStart = m.Index;
Int32 iLen = m.Length;
REGEX_Snapshot.Remove(ref iStart, ref iLen);
REGEX_Snapshot.Select(CursorPosition.Start, iStart, 0);
REGEX_Snapshot.InsertString(EDIT_Replace.Text);
REGEX_Snapshot.Select(CursorPosition.Start,
REGEX_Snapshot.SelectionStart - EDIT_Replace.Text.Length,
EDIT_Replace.Text.Length);
}
即使这看起来很奇怪,但它是实现查找和替换的一种非常一致的方式。想象一下,字母之间有您不知道的自定义对象。快照会处理这种情况,您不必担心。
3. 安全内容序列化
用户键入并格式化文本后,您需要保存它。我的控件提供了一种安全的方式来实现这一点
private void BTN_Serialize_Click(object sender, RoutedEventArgs e)
{
MemoryStream Buffer = new MemoryStream();
RichTextEdit.Snapshot Snapshot = RichEdit.QuerySelectionText();
Snapshot.Serialize(false, Buffer);
// convert to base64
LABEL_Binary.Text = Convert.ToBase64String(Buffer.GetBuffer(), 0, (int)Buffer.Length);
BTN_Deserialize.IsEnabled = true;
}
如您所见,快照再次涉及其中;您需要一个要序列化的快照。如果您的Web服务器不支持二进制序列化,只需像上面那样将其编码为Base64。所有格式、所有宏和所有自定义富文本对象都将包含在此序列化流中。
反序列化工作类似
private void BTN_Deserialize_Click(object sender, RoutedEventArgs e)
{
MemoryStream Buffer = new
MemoryStream(Convert.FromBase64String(LABEL_Binary.Text));
RichEdit.InsertDeserialization(false, Buffer);
}
3.1 为什么它是安全的?
它是安全的,因为它100%可验证。安全并不意味着它是加密的;您仍然需要使用SSL进行加密内容传输。可验证性可以防止您受到一系列常见攻击,因为不可能在服务器上存储有害的序列化内容,或者在访问您的网站时调用有害的操作。
在未来的版本中,序列化将得到改进,将进行压缩,这将大大减小最终流的大小并降低您的存储成本。
4. 自定义富文本对象
即使您可以插入普通的框架元素(FE),我也不推荐这样做。普通的FE不包含在任何序列化或剪贴板操作中。为了让FE能够被序列化,您需要创建一个实现“IRichTextObject
”接口的类。下面是一个围绕简单按钮的富文本对象包装器
class SerializableButton : UserControl, IRichTextObject
{
private String m_Caption;
private Button m_Instance;
private SerializableButton() : base() { }
public SerializableButton(String InCaption)
{
m_Caption = InCaption;
}
public Int16 GetTypeID() { return 0x100; }
public Boolean IsFocusable { get { return true; } }
public void Serialize(FrameworkElement InElement, BinaryWriter InTarget)
{
SerializableButton Button = (SerializableButton)InElement;
InTarget.Write((String)Button.m_Instance.Content);
}
public FrameworkElement Deserialize(
Boolean InIgnoreWarnings,
BinaryReader InSource)
{
return CreateInstance(InSource.ReadString());
}
public FrameworkElement CreateInstance()
{
return CreateInstance(m_Caption);
}
private FrameworkElement CreateInstance(String InCaption)
{
if (InCaption == null)
throw new InvalidOperationException();
SerializableButton Result = new SerializableButton();
Result.m_Instance = new Button();
Result.m_Instance.Content = InCaption;
Result.Content = Result.m_Instance;
Result.Width = 100;
Result.Height = 25;
return Result;
}
}
上面的代码结合了框架元素和接口。一般来说,这是最简单的方法,因为在序列化和反序列化期间,您只会得到一个FE的引用,而找到合适的富文本对象接口是您的职责。
实现接口的类应该有一个构造函数,该构造函数接受实例化它所需的所有参数。在本例中,我们只需要一个按钮的标题。这些也是应该被序列化的参数,因为这样,您只需要反序列化并将它们传递给构造函数即可反序列化整个富文本对象。
实例创建者有些特别。富文本对象(RTO)可以作为包装器围绕普通框架元素进行处理。它提供了一种通过序列化保留所有必需数据的方法,以便稍后重新创建技术上相同的实例。RTO实例应始终通过“CreateInstance
”提供技术上相同的框架元素,但切勿返回任何对象两次。它应该只确保所有指向同一RTO构造函数参数的对象都具有相同的行为和外观。
在此之前,您必须将RTO注册到您的编辑器,如初始化所示
RichEdit = new RichTextEdit();
…
RichEdit.RegisterObject(new SerializableButton(null));
您不需要传递任何参数,因为此RTO实例应该只提供可以作为静态方法处理的“Serialize
”、“Deserialize
”和“GetTypeID
”方法,但接口不支持静态方法……
这项技术很难解释,但如果理解了,它将非常强大。请查看演示并尝试找出它是如何工作的。
玩得开心!