c# winforms .net-4.0

c# - No levante TextChanged mientras escribe continuamente



winforms .net-4.0 (7)

¿No puedes hacer algo en las siguientes líneas?

Stopwatch stopWatch; TextBoxEnterHandler(...) { stopwatch.ReStart(); } TextBoxExitHandler(...) { stopwatch.Stop(); } TextChangedHandler(...) { if (stopWatch.ElapsedMiliseconds < threshHold) { stopwatch.Restart(); return; } { //Update code } stopwatch.ReStart() }

Tengo un cuadro de texto que tiene un controlador de eventos _TextChanged bastante fuerte. En condiciones de tipeo normales, el rendimiento es correcto, pero puede retrasarse notablemente cuando el usuario realiza una acción continua larga, como mantener presionado el botón de retroceso para eliminar una gran cantidad de texto a la vez.

Por ejemplo, el evento tardó 0.2 segundos en completarse, pero el usuario está realizando una eliminación cada 0.1 segundos. Por lo tanto, no puede ponerse al día y habrá una acumulación de eventos que deben ser manejados, lo que lleva a la IU rezagada.

Sin embargo, el evento no necesita ejecutarse para estos estados intermedios, porque solo se preocupa por el resultado final. ¿Hay alguna manera de que el controlador de eventos sepa que debe procesar solo el último evento e ignorar todos los cambios obsoletos previos?


Me he encontrado con este problema varias veces según la experiencia. Encontré esta solución simple y ordenada hasta el momento. Está funcionando en Windows Form pero puede convertirse fácilmente a WPF .

Cómo funciona:

Cuando se informa a un objeto de TypeAssistant que se ha producido un text change , se ejecutará un temporizador. Luego, después de WaitingMilliSeconds , el temporizador levantará Idle evento Idle . Al manejar este evento, puede hacer su trabajo. Si, mientras tanto, se produce otro text change , el temporizador se restablece.

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

Uso:

public partial class Form1 : Form { TypeAssistant assistant; public Form1() { InitializeComponent(); assitant = new TypeAssistant(); assitant.Idled += assitant_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(); } }

Ventajas:

  • ¡Sencillo!
  • Trabajando tanto en WPF como en Windows Form
  • Trabajando con .Net Framework 3.5+

Desventajas:

  • Ejecuta un hilo más
  • Necesita Invocación en lugar de manipulación directa de forma

No sé cómo descartar la cola de eventos, pero puedo pensar en dos formas en que puede manejar esto.

Si quiere algo rápido (y ligeramente sucio según los estándares de algunas personas), podría introducir un tipo de temporizador de espera: cuando se ejecuta la función de validación, establezca un indicador (la variable estática dentro de la función debería ser suficiente) con la hora actual. si la función se vuelve a llamar dentro de, por ejemplo, 0,5 segundos de la última vez que se ejecutó y finalizó, salga de la función de inmediato (reduciendo drásticamente el tiempo de ejecución de la función). Esto resolverá el retraso acumulado de eventos, siempre que sea el contenido de la función lo que causa que sea lento en lugar del disparo del evento en sí. La desventaja de esto es que tendría que introducir una verificación de respaldo de algún tipo para asegurarse de que el estado actual haya sido validado, es decir, si el último cambio tuvo lugar mientras estaba ocurriendo un bloqueo de 0.5 s.

Alternativamente, si su único problema es que no desea que la validación se produzca cuando el usuario está participando en una acción continua, podría intentar modificar su controlador de eventos de forma tal que salga sin realizar la validación si hay una tecla presionada, o tal vez incluso vincule la acción de validación con KeyUp en lugar de TextChanged.

Hay muchas formas en que podrías lograr esto. Por ejemplo, si se realiza un evento KeyDown en una tecla en particular (digamos retroceso para su ejemplo, pero en teoría debe extenderlo a cualquier cosa que escriba un carácter), la función de validación se cerrará sin hacer nada hasta que el evento KeyUp de la la misma llave es disparada De esa forma no se ejecutará hasta que se haya realizado la última modificación ... con suerte.

Esa puede no ser la forma más óptima de lograr el efecto deseado (¡puede que no funcione en absoluto!) Existe la posibilidad de que el evento _TextChanged se active antes de que el usuario haya terminado de presionar la tecla), pero la teoría es sólida. Sin perder el tiempo jugando, no puedo estar absolutamente seguro del comportamiento de la pulsación de la tecla. ¿Puede verificar si la tecla está presionada y salir, o tiene que levantar una bandera manualmente que será verdadera entre KeyDown y KeyUp? Un poco de jugar con sus opciones debería dejar bastante claro cuál será el mejor enfoque para su caso particular.

¡Espero que eso ayude!


Puede marcar su controlador de eventos como una async y hacer lo siguiente:

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

La cláusula try-finally es obligatoria para garantizar que isBusyProcessing esté configurado como false en algún momento, para que no termine en un bucle infinito.


También creo que las extensiones reactivas son el camino a seguir aquí. Aunque tengo una consulta ligeramente diferente.

Mi código se ve así:

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

Ahora esto funciona exactamente de la manera que estabas anticipando.

FromEventPattern traduce el TextChanged en un observable que devuelve el remitente y los eventos args. Select luego los cambia al texto actual en el TextBox . Throttle ignora básicamente las pulsaciones de teclas anteriores si se produce una nueva dentro de los 300 milisegundos, de modo que solo se transfiere la última pulsación de tecla dentro de la ventana de 300 milisegundos. El Select luego llama al procesamiento.

Ahora, aquí está la magia. El Switch hace algo especial. Como la selección IObservable<IObservable<string>> un observable, tenemos, antes del Switch , un IObservable<IObservable<string>> . El Switch toma solo lo último producido observable y produce los valores a partir de él. Esto es de crucial importancia. Esto significa que si el usuario escribe una pulsación de tecla mientras se está ejecutando el procesamiento existente, ignorará ese resultado cuando se presente y solo informará el resultado del último procesamiento de ejecución.

Finalmente hay un ObserveOn para devolver la ejecución al subproceso de la interfaz de usuario, y luego está el Subscribe para manejar realmente el resultado, y en mi caso actualizar el texto en un segundo TextBox .

Creo que este código es increíblemente limpio y muy poderoso. Puede obtener Rx usando Nuget para "Rx-WinForms".


Use una combinación de TextChanged con una comprobación de enfoque y TextLeave.

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


Las extensiones reactivas se ocupan muy bien de este tipo de escenarios.

Por lo tanto, desea capturar el evento TextChanged al estrangularlo durante 0.1 segundos y procesar la entrada. Puede convertir sus eventos TextChanged en IObservable<string> y suscribirse.

Algo como esto

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

Entonces, este fragmento de código se suscribe a eventos TextChanged , lo TextChanged , se asegura de que solo obtenga valores distintos y luego extrae los valores de Text de los eventos args.

Tenga en cuenta que este código es más como un pseudocódigo, no lo probé. Para usar Rx Linq , necesitará instalar el paquete Rx-Linq Nuget .

Si te gusta este enfoque, puedes consultar esta publicación de blog que implementa control automático completo utilizando Rx Linq. También recomendaría una gran charla sobre Bart De Smet en Reactive Extensions.