如何阻止RichTextBox内联在TextChanged中被删除?

OhB*_*ise 5 c# wpf richtextbox

我的任务是创建一个部分可编辑的RichTextBox.我已经在Xaml中看到了TextBlock为这些ReadOnly部分添加元素的建议,但是它具有不能很好地包装的不期望的视觉效果.(它应该显示为单个连续文本块.)

我使用一些反向字符串格式化修补了一个工作原型来限制/允许编辑,并将其与动态创建内联Run元素结合起来用于显示目的.使用字典存储文本的可编辑部分的当前值,我会Run根据任何TextChanged事件触发器相应地更新元素- 并且如果可编辑部分的文本被完全删除,它将被替换回其默认值.

在字符串中:"嗨,NAME,欢迎来到SPORT营地." ,只有NAMESPORT可编辑.

                ??????????????????                    ??????????????????
Default values: ? Key   ? Value  ?    Edited values:  ? Key   ? Value  ?
                ??????????????????                    ??????????????????
                ? NAME  ? NAME   ?                    ? NAME  ? John   ?
                ? SPORT ? SPORT  ?                    ? SPORT ? Tennis ?
                ??????????????????                    ??????????????????

 "Hi NAME, welcome to SPORT camp."    "Hi John, welcome to Tennis camp."
Run Code Online (Sandbox Code Playgroud)

问题:

删除特定运行中的整个文本值将从中删除该运行(以及以下运行)RichTextBox Document.即使我将它们全部添加回来,它们也不再在屏幕上正确显示.例如,使用上面设置中的已编辑字符串:

  • 用户突出显示文本"John"并单击Delete,而不是保存空值,它应替换为默认文本"NAME".在内部发生这种情况.字典获取正确的值,Run.Text具有值,Document包含所有正确的Run元素.但屏幕显示:

    • 预计:"嗨,NAME,欢迎来到网球营."
    • 实际:"嗨,NAMETennis阵营."

实际与预期的截图

旁注:粘贴时,这种运行元素丢失行为也可以重复.突出显示"SPORT"并粘贴"Tennis"和包含"阵营"的Run .迷路了.

问题:

Run一旦更换后,即使通过破坏性行为,我如何保持每个元素的可见性?

代码:

我试图将代码删除到最小的例子,所以我删除了:

  • DependencyPropertyxaml中的每个和相关的绑定
  • 逻辑重新计算插入位置(对不起)
  • 重构了链接字符串格式化从所述第一链路包含一个单一的方法扩展方法的类.(注意:此方法适用于简单的示例字符串格式.我的代码用于更强大的格式化已被排除.所以请坚持为这些测试目的提供的示例.)
  • 使可编辑的部分清晰可见,无需调整配色方案.

要进行测试,请将该类放入WPF项目的Resource文件夹中,修复命名空间,然后将控件添加到View中.

using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;

namespace WPFTest.Resources
{
  public class MyRichTextBox : RichTextBox
  {
    public MyRichTextBox()
    {
      this.TextChanged += MyRichTextBox_TextChanged;
      this.Background = Brushes.LightGray;

      this.Parameters = new Dictionary<string, string>();
      this.Parameters.Add("NAME", "NAME");
      this.Parameters.Add("SPORT", "SPORT");

      this.Format = "Hi {0}, welcome to {1} camp.";
      this.Text = string.Format(this.Format, this.Parameters.Values.ToArray<string>());

      this.Runs = new List<Run>()
      {
        new Run() { Background = Brushes.LightGray, Tag = "Hi " },
        new Run() { Background = Brushes.Black, Foreground = Brushes.White, Tag = "NAME" },
        new Run() { Background = Brushes.LightGray, Tag = ", welcome to " },
        new Run() { Background = Brushes.Black, Foreground = Brushes.White, Tag = "SPORT" },
        new Run() { Background = Brushes.LightGray, Tag = " camp." },
      };

      this.UpdateRuns();
    }

    public Dictionary<string, string> Parameters { get; set; }
    public List<Run> Runs { get; set; }
    public string Text { get; set; }
    public string Format { get; set; }

    private void MyRichTextBox_TextChanged(object sender, TextChangedEventArgs e)
    {
      string richText = new TextRange(this.Document.Blocks.FirstBlock.ContentStart, this.Document.Blocks.FirstBlock.ContentEnd).Text;
      string[] oldValues = this.Parameters.Values.ToArray<string>();
      string[] newValues = null;

      bool extracted = this.TryParseExact(richText, this.Format, out newValues);

      if (extracted)
      {
        var changed = newValues.Select((x, i) => new { NewVal = x, Index = i }).Where(x => x.NewVal != oldValues[x.Index]).FirstOrDefault();
        string key = this.Parameters.Keys.ElementAt(changed.Index);
        this.Parameters[key] = string.IsNullOrWhiteSpace(newValues[changed.Index]) ? key : newValues[changed.Index];

        this.Text = richText;
      }
      else
      {
        e.Handled = true;
      }

      this.UpdateRuns();
    }

    private void UpdateRuns()
    {
      this.TextChanged -= this.MyRichTextBox_TextChanged;

      foreach (Run run in this.Runs)
      {
        string value = run.Tag.ToString();

        if (this.Parameters.ContainsKey(value))
        {
          run.Text = this.Parameters[value];
        }
        else
        {
          run.Text = value;
        }
      }

      Paragraph p = this.Document.Blocks.FirstBlock as Paragraph;
      p.Inlines.Clear();
      p.Inlines.AddRange(this.Runs);

      this.TextChanged += this.MyRichTextBox_TextChanged;
    }

    public bool TryParseExact(string data, string format, out string[] values)
    {
      int tokenCount = 0;
      format = Regex.Escape(format).Replace("\\{", "{");
      format = string.Format("^{0}$", format);

      while (true)
      {
        string token = string.Format("{{{0}}}", tokenCount);

        if (!format.Contains(token))
        {
          break;
        }

        format = format.Replace(token, string.Format("(?'group{0}'.*)", tokenCount++));
      }

      RegexOptions options = RegexOptions.None;

      Match match = new Regex(format, options).Match(data);

      if (tokenCount != (match.Groups.Count - 1))
      {
        values = new string[] { };
        return false;
      }
      else
      {
        values = new string[tokenCount];

        for (int index = 0; index < tokenCount; index++)
        {
          values[index] = match.Groups[string.Format("group{0}", index)].Value;
        }

        return true;
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

Yus*_*dın 2

您的代码的问题在于,当您通过用户界面更改文本时,内部Run对象会被修改、创建、删除,并且所有疯狂的事情都会在幕后发生。内部结构非常复杂。例如,这里有一个在无辜的单行深处被调用的方法p.Inlines.Clear();

private int DeleteContentFromSiblingTree(SplayTreeNode containingNode, TextPointer startPosition, TextPointer endPosition, bool newFirstIMEVisibleNode, out int charCount)
{
    SplayTreeNode leftSubTree;
    SplayTreeNode middleSubTree;
    SplayTreeNode rightSubTree;
    SplayTreeNode rootNode;
    TextTreeNode previousNode;
    ElementEdge previousEdge;
    TextTreeNode nextNode;
    ElementEdge nextEdge;
    int symbolCount;
    int symbolOffset;

    // Early out in the no-op case. CutContent can't handle an empty content span.
    if (startPosition.CompareTo(endPosition) == 0)
    {
        if (newFirstIMEVisibleNode)
        {
            UpdateContainerSymbolCount(containingNode, /* symbolCount */ 0, /* charCount */ -1);
        }
        charCount = 0;
        return 0;
    }

    // Get the symbol offset now before the CutContent call invalidates startPosition.
    symbolOffset = startPosition.GetSymbolOffset();

    // Do the cut.  middleSubTree is what we want to remove.
    symbolCount = CutContent(startPosition, endPosition, out charCount, out leftSubTree, out middleSubTree, out rightSubTree);

    // We need to remember the original previous/next node for the span
    // we're about to drop, so any orphaned positions can find their way
    // back.
    if (middleSubTree != null)
    {
        if (leftSubTree != null)
        {
            previousNode = (TextTreeNode)leftSubTree.GetMaxSibling();
            previousEdge = ElementEdge.AfterEnd;
        }
        else
        {
            previousNode = (TextTreeNode)containingNode;
            previousEdge = ElementEdge.AfterStart;
        }
        if (rightSubTree != null)
        {
            nextNode = (TextTreeNode)rightSubTree.GetMinSibling();
            nextEdge = ElementEdge.BeforeStart;
        }
        else
        {
            nextNode = (TextTreeNode)containingNode;
            nextEdge = ElementEdge.BeforeEnd;
        }

        // Increment previous/nextNode reference counts. This may involve
        // splitting a text node, so we use refs.
        AdjustRefCountsForContentDelete(ref previousNode, previousEdge, ref nextNode, nextEdge, (TextTreeNode)middleSubTree);

        // Make sure left/rightSubTree stay local roots, we might
        // have inserted new elements in the AdjustRefCountsForContentDelete call.
        if (leftSubTree != null)
        {
            leftSubTree.Splay();
        }
        if (rightSubTree != null)
        {
            rightSubTree.Splay();
        }
        // Similarly, middleSubtree might not be a local root any more,
        // so splay it too.
        middleSubTree.Splay();

        // Note TextContainer now has no references to middleSubTree, if there are
        // no orphaned positions this allocation won't be kept around.
        Invariant.Assert(middleSubTree.ParentNode == null, "Assigning fixup node to parented child!");
        middleSubTree.ParentNode = new TextTreeFixupNode(previousNode, previousEdge, nextNode, nextEdge);
    }

    // Put left/right sub trees back into the TextContainer.
    rootNode = TextTreeNode.Join(leftSubTree, rightSubTree);
    containingNode.ContainedNode = rootNode;
    if (rootNode != null)
    {
        rootNode.ParentNode = containingNode;
    }

    if (symbolCount > 0)
    {
        int nextNodeCharDelta = 0;
        if (newFirstIMEVisibleNode)
        {
            // The following node is the new first ime visible sibling.
            // It just moved, and loses an edge character.
            nextNodeCharDelta = -1;
        }

        UpdateContainerSymbolCount(containingNode, -symbolCount, -charCount + nextNodeCharDelta);
        TextTreeText.RemoveText(_rootNode.RootTextBlock, symbolOffset, symbolCount);
        NextGeneration(true /* deletedContent */);

        // Notify the TextElement of a content change. Note that any full TextElements
        // between startPosition and endPosition will be handled by CutTopLevelLogicalNodes,
        // which will move them from this tree to their own private trees without changing
        // their contents.
        Invariant.Assert(startPosition.Parent == endPosition.Parent);
        TextElement textElement = startPosition.Parent as TextElement;
        if (textElement != null)
        {               
            textElement.OnTextUpdated();                    
        }
    }

    return symbolCount;
}
Run Code Online (Sandbox Code Playgroud)

如果您有兴趣,可以从这里查看源代码。

解决方案是不要Run直接在FlowDocument. 在添加它们之前,请务必先复制一份:

private void UpdateRuns()
{
    TextChanged -= MyRichTextBox_TextChanged;

    List<Run> runs = new List<Run>();
    foreach (Run run in Runs)
    {
        Run newRun;
        string value = run.Tag.ToString();

        if (Parameters.ContainsKey(value))
        {
            newRun = new Run(Parameters[value]);
        }
        else
        {
            newRun = new Run(value);
        }

        newRun.Background = run.Background;
        newRun.Foreground = run.Foreground;

        runs.Add(newRun);
    }

    Paragraph p = Document.Blocks.FirstBlock as Paragraph;
    p.Inlines.Clear();
    p.Inlines.AddRange(runs);

    TextChanged += MyRichTextBox_TextChanged;
}
Run Code Online (Sandbox Code Playgroud)