You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

MAUI Editor TextChanged事件异常触发问题求助

文本编辑器TextChanged事件批量删除选中内容时命令记录异常问题

问题描述

以下是编辑器的TextChanged事件代码,用于记录文本的插入/删除操作到命令列表:

private void FormulaEditor_TextChanged(object sender, TextChangedEventArgs e)
{
    if(!changedByControls)
    {
        int oldTextLength = e.OldTextValue?.Length ?? 0;
        int newTextLength = e.NewTextValue?.Length ?? 0;           

        bool textInserted = newTextLength > oldTextLength;

        TextAction action = textInserted ? TextAction.Insert : TextAction.Remove;
        int textLength = Math.Abs(newTextLength - oldTextLength);
        int startIndex = textInserted ? FormulaEditor.CursorPosition : FormulaEditor.CursorPosition - textLength;

        string textForOperation = textInserted ? e.NewTextValue: e.OldTextValue;
        string text = textForOperation.Substring(startIndex, textLength);

        textCommandManager.ChangeText(action, text, startIndex);
        UndoButton.IsEnabled = textCommandManager.CanUndo;
    }
    
    changedByControls = false;         
}

插入单个字符、删除单个字符或全选删除时均正常,但删除选中的部分文本时出现异常:例如粘贴"wasabi"后,选中"sabi"部分删除,TextChanged事件会触发3次,命令列表新增3条操作(预期仅1条Remove操作),最终列表为:

indexOfElement - 0, action - Insert, Position - 0, Text - "wasabi";
indexOfElement - 1, action - Remove, Position - 2, Text - "sabi"; 
indexOfElement - 2, action - Remove, Position - 0, Text - "wa"; 
indexOfElement - 3, action - Insert, Position - 0, Text - "wa"; 

异常原因

  1. 控件内部多步操作触发多次TextChanged:以WPF TextBox为例,删除选中内容时,控件内部可能执行多步逻辑(比如先清空选中状态、再删除文本,甚至同步内部撤销栈),每一步都会触发TextChanged事件,导致代码重复记录操作。
  2. 依赖光标位置计算变更不可靠:批量删除后,光标会直接跳到删除后的起始位置,用FormulaEditor.CursorPosition计算删除操作的起始索引会完全错误,进而生成错误的命令记录。
  3. 未合并连续的同类型操作:多次触发的TextChanged事件对应同一次用户操作,但代码每次都独立记录,没有做合并处理。

修复方案

方案1:通过TextChangedEventArgs.Changes获取准确变更

每个TextChangedEventArgs包含的Changes集合会列出本次事件触发的所有文本变更,直接遍历这个集合可以准确获取每一次变更的位置、删除长度和插入长度,避免依赖光标位置。

修复后的代码:

private void FormulaEditor_TextChanged(object sender, TextChangedEventArgs e)
{
    if (!changedByControls)
    {
        foreach (var change in e.Changes)
        {
            if (change.AddedLength > 0)
            {
                // 插入操作:从当前文本中提取插入内容
                string insertedText = FormulaEditor.Text.Substring(change.Offset, change.AddedLength);
                textCommandManager.ChangeText(TextAction.Insert, insertedText, change.Offset);
            }
            else if (change.RemovedLength > 0)
            {
                // 删除操作:从旧文本中提取删除内容
                string removedText = e.OldTextValue.Substring(change.Offset, change.RemovedLength);
                textCommandManager.ChangeText(TextAction.Remove, removedText, change.Offset);
            }
        }
        UndoButton.IsEnabled = textCommandManager.CanUndo;
    }
    
    changedByControls = false;         
}

方案2:延迟合并操作(通用解决方案)

如果控件的Changes集合无法准确反映用户操作,或者仍存在多次触发的情况,可以用延迟执行的方式,等待所有内部操作完成后再一次性记录合并后的操作。

步骤:

  1. 初始化一个DispatcherTimer,用于延迟处理命令记录。
  2. 每次TextChanged触发时,重置定时器。
  3. 定时器到期后,对比当前文本与上一次记录的文本,计算出最终的变更并记录。

代码示例:

private DispatcherTimer _commandMergeTimer;
private string _lastRecordedText;

// 在窗口/控件构造函数中初始化
public YourEditorControl()
{
    InitializeComponent();
    _commandMergeTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) };
    _commandMergeTimer.Tick += CommandMergeTimer_Tick;
    // 初始化记录的文本为编辑器初始内容
    _lastRecordedText = FormulaEditor.Text;
}

private void FormulaEditor_TextChanged(object sender, TextChangedEventArgs e)
{
    if (!changedByControls)
    {
        // 重置定时器,确保等待所有内部操作完成
        _commandMergeTimer.Stop();
        _commandMergeTimer.Start();
    }
    
    changedByControls = false;         
}

private void CommandMergeTimer_Tick(object sender, EventArgs e)
{
    _commandMergeTimer.Stop();
    string currentText = FormulaEditor.Text;

    if (currentText == _lastRecordedText) return;

    // 计算文本差异
    if (currentText.Length > _lastRecordedText.Length)
    {
        // 处理插入操作:找到第一个不同的位置,提取新增内容
        int insertOffset = 0;
        while (insertOffset < _lastRecordedText.Length && currentText[insertOffset] == _lastRecordedText[insertOffset])
        {
            insertOffset++;
        }
        string insertedText = currentText.Substring(insertOffset);
        textCommandManager.ChangeText(TextAction.Insert, insertedText, insertOffset);
    }
    else if (currentText.Length < _lastRecordedText.Length)
    {
        // 处理删除操作:找到第一个不同的位置,提取删除内容
        int removeOffset = 0;
        while (removeOffset < currentText.Length && currentText[removeOffset] == _lastRecordedText[removeOffset])
        {
            removeOffset++;
        }
        int removeLength = _lastRecordedText.Length - currentText.Length;
        string removedText = _lastRecordedText.Substring(removeOffset, removeLength);
        textCommandManager.ChangeText(TextAction.Remove, removedText, removeOffset);
    }

    _lastRecordedText = currentText;
    UndoButton.IsEnabled = textCommandManager.CanUndo;
}

方案选择

  • 方案1优先使用,因为它直接利用控件提供的变更信息,性能更高且准确性好,适合大多数标准WPF文本控件场景。
  • 方案2适合自定义编辑器控件,或者控件内部逻辑复杂导致Changes集合无法准确反映用户操作的场景。

内容的提问来源于stack exchange,提问作者Death_Dealer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 21:14:57