Подтвердить что ты не робот

Не повышать TextChanged при непрерывном наборе текста

У меня есть текстовое поле с довольно тяжелым обработчиком событий _TextChanged. В нормальном режиме типизации производительность в порядке, но она может заметно отставать, когда пользователь выполняет длительное непрерывное действие, например, удерживая кнопку backspace нажатой, чтобы удалить сразу много текста.

Например, для завершения мероприятия потребовалось 0,2 секунды, но пользователь выполняет одно удаление каждые 0,1 секунды. Таким образом, он не может догнать и произойдет отставание от событий, которые необходимо обработать, что приведет к отставанию пользовательского интерфейса.

Однако для этих промежуточных состояний событие не нужно запускать, потому что оно заботится только о конечном результате. Есть ли способ сообщить обработчику событий, что он должен обрабатывать только последнее событие и игнорировать все предыдущие устаревшие изменения?

4b9b3361

Ответ 1

Я сталкивался с этой проблемой несколько раз, основываясь на опыте, который пока нашел это решение простым и аккуратным. Он работает в Windows Form, но может быть легко преобразован в WPF.

Как это работает:

Когда объект TypeAssistant получает информацию о том, что text change произошел, он запускает таймер. После WaitingMilliSeconds таймер вызывает событие Idleی. Управляя этим событием, вы можете выполнять любую работу, какую пожелаете. Если в промежуток времени, начинающийся с начала таймера, а затем WaitingMilliSeconds возникает другой text change, таймер сбрасывается.

public class TypeAssistant
{
    public event EventHandler Idled = delegate { };
    public int WaitingMilliSeconds { get; set; }
    System.Threading.Timer waitingTimer;

    public TypeAssistant(int waitingMilliSeconds = 600)
    {
        WaitingMilliSeconds = waitingMilliSeconds;
        waitingTimer = new Timer(p =>
        {
            Idled(this, EventArgs.Empty);
        });
    }
    public void TextChanged()
    {
        waitingTimer.Change(WaitingMilliSeconds, System.Threading.Timeout.Infinite);
    }
}

Использование:

public partial class Form1 : Form
{
    TypeAssistant assistant;
    public Form1()
    {
        InitializeComponent();
        assistant = new TypeAssistant();
        assistant.Idled += assistant_Idled;          
    }

    void assistant_Idled(object sender, EventArgs e)
    {
        this.Invoke(
        new MethodInvoker(() =>
        {
            // do your job here
        }));
    }

    private void yourFastReactingTextBox_TextChanged(object sender, EventArgs e)
    {
        assistant.TextChanged();
    }
}

Преимущества:

  • Просто!
  • Работает как в WPF, так и в Windows Form
  • Работа с .Net Framework 3. 5+

Недостатки:

  • Запускает еще один поток
  • Требуется вызов вместо прямого манипулирования формой

Ответ 2

Я также думаю, что Reactive Extensions - это путь сюда. У меня есть несколько другой запрос, хотя.

Мой код выглядит следующим образом:

        IDisposable subscription =
            Observable
                .FromEventPattern(
                    h => textBox1.TextChanged += h,
                    h => textBox1.TextChanged -= h)
                .Select(x => textBox1.Text)
                .Throttle(TimeSpan.FromMilliseconds(300))
                .Select(x => Observable.Start(() => /* Do processing */))
                .Switch()
                .ObserveOn(this)
                .Subscribe(x => textBox2.Text = x);

Теперь это работает точно так, как вы ожидали.

FromEventPattern переводит TextChanged в наблюдаемое, которое возвращает атрибуты отправителя и события. Select затем изменяет их на фактический текст в TextBox. Throttle в основном игнорирует предыдущие нажатия клавиш, если новый встречается в течение 300 миллисекунд - так, что передается только последнее нажатие клавиши, нажатое в течение прокатного 300 миллисекундного окна. Затем Select вызывает обработку.

Теперь, вот волшебство. Switch делает что-то особенное. Поскольку выбор вернул наблюдаемый, мы имеем перед Switch, a IObservable<IObservable<string>>. Switch принимает только последние произведенные наблюдаемые и выдает значения из него. Это имеет решающее значение. Это означает, что если пользователь набирает нажатие клавиши во время текущей обработки, он будет игнорировать этот результат, когда он появится, и будет когда-либо сообщать о результатах последней обработки выполнения.

Наконец, a ObserveOn, чтобы вернуть выполнение в поток пользовательского интерфейса, а затем Subscribe, чтобы обработать результат, - и в моем случае обновите текст на второй TextBox.

Я думаю, что этот код невероятно опрятен и очень силен. Вы можете получить Rx, используя Nuget для "Rx-WinForms".

Ответ 3

Один простой способ - использовать async/await для внутреннего метода или делегирования:

private async void textBox1_TextChanged(object sender, EventArgs e) {
    // this inner method checks if user is still typing
    async Task<bool> UserKeepsTyping() {
        string txt = textBox1.Text;   // remember text
        await Task.Delay(500);        // wait some
        return txt != textBox1.Text;  // return that text chaged or not
    }
    if (await UserKeepsTyping()) return;
    // user is done typing, do your stuff    
}

Здесь нет потоков. Для версии С# старше 7.0 вы можете объявить делегата:

Func<Task<bool>> UserKeepsTyping = async delegate () {...}

Обратите внимание, что этот метод не позволит вам иногда обрабатывать один и тот же "конечный результ" дважды. Например. когда пользователь набирает "ab", а затем сразу же удаляет "b", вы можете завершить обработку "a" дважды. Но эти случаи довольно редко. Чтобы избежать их, код может быть таким:

// last processed text
string lastProcessed;
private async void textBox1_TextChanged(object sender, EventArgs e) {
    // clear last processed text if user deleted all text
    if (string.IsNullOrEmpty(textBox1.Text)) lastProcessed = null;
    // this inner method checks if user is still typing
    async Task<bool> UserKeepsTyping() {
        string txt = textBox1.Text;   // remember text
        await Task.Delay(500);        // wait some
        return txt != textBox1.Text;  // return that text chaged or not
    }
    if (await UserKeepsTyping() || textBox1.Text == lastProcessed) return;
    // save the text you process, and do your stuff
    lastProcessed = textBox1.Text;   
}

Ответ 4

Вы можете пометить обработчик событий как async и сделать следующее:

bool isBusyProcessing = false;

private async void textBox1_TextChanged(object sender, EventArgs e)
{
    while (isBusyProcessing)
        await Task.Delay(50);

    try
    {
        isBusyProcessing = true;
        await Task.Run(() =>
        {
            // Do your intensive work in a Task so your UI doesn't hang
        });

    }
    finally
    {
        isBusyProcessing = false;
    }
}
Предложение

Try try-finally является обязательным для обеспечения того, чтобы в какой-то момент isBusyProcessing было задано значение false, так что вы не закончите бесконечный цикл.

Ответ 5

Reactive Extensions очень хорошо справляются с такими сценариями.

Итак, вы хотите захватить событие TextChanged, отрегулировав его на 0.1 секунды и обработать ввод. Вы можете преобразовать события TextChanged в IObservable<string> и подписаться на него.

Что-то вроде этого

(from evt in Observable.FromEventPattern(textBox1, "TextChanged")
 select ((TextBox)evt.Sender).Text)
.Throttle(TimeSpan.FromMilliSeconds(90))
.DistinctUntilChanged()
.Subscribe(result => // process input);

Таким образом, этот фрагмент кода подписывается на события TextChanged, дросселирует его, гарантирует, что вы получите только разные значения, а затем вытащите значения Text из аргументов событий.

Обратите внимание, что этот код больше похож на псевдокод, я его не тестировал. Чтобы использовать Rx Linq, вам нужно будет установить пакет Rx-Linq Nuget.

Если вам нравится этот подход, вы можете проверить этот пост в блоге, который реализует автоматическое полное управление с использованием Rx Linq. Я бы также рекомендовал отличный разговор о Барте Де Смете о реактивных расширениях.

Ответ 6

Используйте комбинацию TextChanged с контролем фокуса и TextLeave.

private void txt_TextChanged(object sender, EventArgs e)
{
    if (!((TextBox)sender).Focused)
        DoWork();
}

private void txt_Leave(object sender, EventArgs e)
{
    DoWork();
}

Ответ 7

Я не знаю об отбрасывании очереди событий, но я могу думать о двух способах, которыми вы можете справиться с этим.

Если вы хотите что-то быстрое (и немного грязное по некоторым стандартам), вы можете ввести таймер ожидания сортировки - когда выполняется функция проверки, установите флаг (статическая переменная в пределах функции должна быть достаточной) с текущим временем. если функция вызывается снова через 0,5 секунды последнего запуска и завершения, немедленно выйдите из функции (значительно сократив время выполнения функции). Это позволит решить отставание от событий, при условии, что это содержимое функции, которая заставляет ее работать медленнее, чем запуск самого события. Недостатком этого является то, что вам придется ввести резервную проверку некоторого вида, чтобы убедиться, что текущее состояние подтверждено - то есть, если последнее изменение имело место, когда выполнялся блок 0,5 с.

В качестве альтернативы, если ваша единственная проблема заключается в том, что вы не хотите, чтобы проверка выполнялась, когда пользователь участвует в непрерывном действии, вы можете попробовать изменить обработчик событий таким образом, чтобы он выходил без выполнения проверки, если нажатие клавиши находится в прогресс или, возможно, даже привязать действие проверки к KeyUp, а не TextChanged.

Существует много способов добиться этого. Например, если событие KeyDown выполняется на определенном ключе (скажем, backspace для вашего примера, но теоретически вы должны распространить его на все, что будет набирать символ), функция проверки будет завершена без каких-либо действий, пока событие KeyUp тот же ключ запускается. Таким образом, он не будет работать до тех пор, пока не будет сделана последняя модификация... надеюсь.

Это может быть не самый оптимальный способ достижения желаемого эффекта (он может вообще не работать! Там шанс, что событие _TextChanged будет срабатывать до того, как пользователь закончит нажатие клавиши), но теория звучит. Не тратя некоторое время на игру, я не могу быть абсолютно уверен в поведении нажатия клавиши - можете ли вы просто проверить, нажата ли клавиша и выйти, или вам нужно поднять флаг вручную, который будет истинным между KeyDown и KeyUp? Немного поиграть с вашими вариантами должно быть достаточно ясно, какой лучший подход будет для вашего конкретного случая.

Я надеюсь, что это поможет!

Ответ 8

Разве вы не можете что-то сделать в следующих строках?

Stopwatch stopWatch;

TextBoxEnterHandler(...)
{
    stopwatch.ReStart();
}

TextBoxExitHandler(...)
{
    stopwatch.Stop();
}

TextChangedHandler(...)
{
    if (stopWatch.ElapsedMiliseconds < threshHold)
    {
        stopwatch.Restart();
        return;
    }

    {
       //Update code
    }

    stopwatch.ReStart()
}

Ответ 9

    private async Task ValidateText()
    {
        if (m_isBusyProcessing)
            return;
        // Don't validate on each keychange
        m_isBusyProcessing = true;
        await Task.Delay(200);
        m_isBusyProcessing = false;

        // Do your work here.       
    }

Ответ 10

Я играл с этим некоторое время. Для меня это было самое элегантное (простое) решение, которое я мог придумать:

    string mostRecentText = "";

    async void entry_textChanged(object sender, EventArgs e)
    {
        //get the entered text
        string enteredText = (sender as Entry).Text;

        //set the instance variable for entered text
        mostRecentText = enteredText;

        //wait 1 second in case they keep typing
        await Task.Delay(1000);

        //if they didn't keep typing
        if (enteredText == mostRecentText)
        {
            //do what you were going to do
            doSomething(mostRecentText);
        }
    }