Evitar se congele el formulario al hacer un for

Iniciado por P4nd3m0n1um, 14 Mayo 2016, 05:04 AM

0 Miembros y 1 Visitante están viendo este tema.

P4nd3m0n1um

Tengo un formulario con un listview y un boton, el botón realiza un proceso con cada item de la lista mediante un for, tengo otro listview que es un log, que cada vez que se ejecuta una linea se envía un aviso al log (se agrega un item a listview log), el problema esta en que este for congela al formulario mientras realiza el proceso, una vez que termina, muestra todo el log y se cumplieron todos los procesos pero necesitaría que el mismo no se congele, ya que el usuario tiene que estar pendiente del log para saber si esta funcionando correctamente o no.

Probe con colocar el log en otro formulario y antes de realizar el for abrir dicho formulario para que este quede activo, pero igual se congela tanto el primero como el segundo.

Alguna idea?

kub0x

Deberías utilizar Threads (hilo) para separar la carga computacional y dejar el main thread (la GUI) sin carga para que se renderice de forma normal, ya que lo que está ocurriendo es que el for invalida o bloquea el proceso de renderizado de la GUI.

https://msdn.microsoft.com/en-us/library/system.threading.thread%28v=vs.110%29.aspx

Saludos!
Viejos siempre viejos,
Ellos tienen el poder,
Y la juventud,
¡En el ataúd! Criaturas Al poder.

Visita mi perfil en ResearchGate


P4nd3m0n1um

Probe con el BackgroundWorker pero me da error al querer completar un listview que esta fuera del proceso:

Código (vbnet) [Seleccionar]
Me.ListView1.Items.Add("Test LOG")

Error:

Código (vbnet) [Seleccionar]
An exception of type 'System.InvalidOperationException' occurred in System.Windows.Forms.dll but was not handled in user code
Additional information: Operación no válida a través de subprocesos: Se tuvo acceso al control 'ListView1' desde un subproceso distinto a aquel en que lo creó.

kub0x

No quería decírtelo para no desalentarte en el proceso de gestión de hilos, pero esto es normal. Me explico, si tienes dos hilos cada uno tiene sus recursos locales y obviamente el hilo de la lógica del for no dispone de los recursos del hilo principal (refiriéndome a los controles ej: ListView) por lo tanto tienes que hacer una llamada desde el hilo del for al main thread para gestionar el control.

¿Cómo lo hacemos? Con delegados, que si programaste alguna vez en C/C++ no son más que pointers a function. Es decir, cuando termines el for en el segundo hilo llamas al function pointer que apunta a una func que reside en el primer hilo, de esta forma intercambias la información.

Ésta es la técnica general propuesta por Microsoft para gestionar dicha comunicación: https://msdn.microsoft.com/en-us/library/zyzhdc6b.aspx (con esto se arregla ;) )

Saludos
Viejos siempre viejos,
Ellos tienen el poder,
Y la juventud,
¡En el ataúd! Criaturas Al poder.

Visita mi perfil en ResearchGate


P4nd3m0n1um

Haber si entendi, usar el BackgroundWorker y cuando se haga el for en vez de mandarle el codigo, llamar a un función:

Código (vbnet) [Seleccionar]
    Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles hiloSegundoPlano.DoWork
        Dim worker As BackgroundWorker = CType(sender, BackgroundWorker)
        Call  PasarList2aList1()
    End Sub

    Function PasarList2aList1(ByVal n As Integer,ByVal worker As BackgroundWorker,ByVal e As DoWorkEventArgs) As Long
        For i = 0 To ListView2.Items.Count - 1
            Dim oreg As New ListViewItem
            Threading.Thread.Sleep("2000")
            oreg = Me.ListView1.Items.Add(ListView2.Items(i).Text)
        Next
        Return
    End Function

kub0x

Cita de: P4nd3m0n1um en 14 Mayo 2016, 07:04 AM
Haber si entendi, usar el BackgroundWorker y cuando se haga el for en vez de mandarle el codigo, llamar a un función

No sé si lo habrás probado, pero no funcionará. Simplemente has separado la lógica del for en otra función (PasarList2aList1).

Lo que tienes que hacer es declarar un delegado que acepte un parámetro de tipo Integer, asignarle la función PasarList2aList1 al objeto delegado e invocarlo desde el BackgroundWorker1_DoWork haciendo Listview1.Invoke(delegado). De esta forma llamas al puntero de la función PasarList2aList1 desde DoWork de manera segura (como pide .NET, c# o vb trabajan igual). Revisa el link que postee antes.

Saludos!
Viejos siempre viejos,
Ellos tienen el poder,
Y la juventud,
¡En el ataúd! Criaturas Al poder.

Visita mi perfil en ResearchGate


fary

Prueba con DoEvents dentro del ciclo for. Así evitarás la sobrecarga, que es lo que hace que se te pete. :P

https://msdn.microsoft.com/es-es/library/system.windows.forms.application.doevents%28v=vs.110%29.aspx

PD: kub0x, al final terminaste escuchando a moraio chico  :laugh: ;-)

saludos!
Un byte a la izquierda.

kub0x

Cita de: fary en 14 Mayo 2016, 09:24 AM
Prueba con DoEvents dentro del ciclo for. Así evitarás la sobrecarga, que es lo que hace que se te pete. :P

Es una solución parcial y no debería de ser implementada en un ámbito profesional o incluso amateur, dado que se considerá mala práctica en el mundo de .NET. Te dirán que .NET no es vb6.

DoEvents permite procesar los eventos que están en la cola del Event Loop, cosa que tu for está ralentizando, de esta forma los mensajes de renderizado del formulario se procesarán y se dibujará sin problemas y sin hilos (todo en el main thread).

Cita de: fary en 14 Mayo 2016, 09:24 AM
PD: kub0x, al final terminaste escuchando a moraio chico  :laugh: ;-)

P.D: Vaya bulerías que se montaba el morao, único en el compás, no he encontrado nada igual, supongo que será intrínseco de Jerez.

Saludos!
Viejos siempre viejos,
Ellos tienen el poder,
Y la juventud,
¡En el ataúd! Criaturas Al poder.

Visita mi perfil en ResearchGate


Eleкtro

#8
Cita de: kub0x en 14 Mayo 2016, 06:33 AMhttps://msdn.microsoft.com/en-us/library/zyzhdc6b.aspx (con esto se arregla ;) )

El compañero @KuBox ya te ha dado y explicado la solución, particionar la lógica del algoritmo en bloques de método individuales es irse por las ramas, ya que sigues sin evaluar la propiedad que se te mencionó.

Pero aparte del uso de la propiedad Control.Invoke, es muy recomendable que también utilices la propiedad Control.InvokeRequired para evitar intentar invocar el control de forma síncrona en caso de que haya sido creado desde el mismo hilo, ya que de lo contrario esto ocasionaría comportamientos indeseados y/o errores, como por ejemplo la recreación del handle del control;

También hay otros controles de errores adicionales que serían bastante imprescindibles como por ejemplo:
· Utilizar la declaración SyncLock para evitar que múltiples hilos intenten modificar el control, por ejemplo si instancias e inicias el mismo BackGroundWorker 2 veces al mismo tiempo.
· Comprobar si los recursos del control fueron liberados (Control.IsDisposed).
· Comprobar si el handle de la ventana se creó, y verificarlo antes de llamar a Control.InvokeRequired, ya que de lo contrario Control.InvokeRequired siempre devolverá False indiferentemente del hilo donde se creó el control.

...Pero por el momento podemos dejarlo con la medida de seguridad básica y principal, que sería, como ya he dicho, evaluar el valor devuelto por Control.InvokeRequired.

Este sería el código que puedes utilizar:
Código (vbnet) [Seleccionar]
Imports System.Threading

Public NotInheritable Class Work : Implements IDisposable

#Region " Private Fields "

   <EditorBrowsable(EditorBrowsableState.Never)>
   Friend WithEvents Bgw As BackgroundWorker

   Private ReadOnly mreSync As ManualResetEvent
   Private ReadOnly mreAsync As ManualResetEvent

   Private isRunSync As Boolean = False
   Private isCancelSyncRequested As Boolean = False
   Private isPauseRequested As Boolean = False

#End Region

#Region " Properties "

   <EditorBrowsable(EditorBrowsableState.Always)>
   Public ReadOnly Property IsRunning As Boolean
       Get
           Return (Me.Bgw IsNot Nothing) AndAlso Not (Me.isPausedB)
       End Get
   End Property

   <EditorBrowsable(EditorBrowsableState.Always)>
   Public ReadOnly Property IsPaused As Boolean
       Get
           Return (Me.Bgw IsNot Nothing) AndAlso (Me.isPausedB)
       End Get
   End Property
   Private isPausedB As Boolean = False

   Public ReadOnly Property IsDisposed As Boolean
       <DebuggerStepThrough>
       Get
           Return (Me.Bgw Is Nothing)
       End Get
   End Property

#End Region

#Region " Constructors "

   <DebuggerNonUserCode>
   Public Sub New()

       Me.Bgw = New BackgroundWorker()
       Me.mreSync = New ManualResetEvent(initialState:=False)
       Me.mreAsync = New ManualResetEvent(initialState:=True)

   End Sub

#End Region

#Region " Public Methods "

   <DebuggerStepThrough>
   Public Sub Run()

       If Not (Me.IsDisposed) AndAlso Not (Me.Bgw.IsBusy) Then
           Me.isRunSync = True
           With Me.Bgw
               .WorkerSupportsCancellation = False
               .WorkerReportsProgress = False
               .RunWorkerAsync()
           End With
           Me.mreSync.WaitOne()

       Else
           Throw New InvalidOperationException("BackGroundWorker is already running.")

       End If

   End Sub

   <DebuggerStepThrough>
   Public Sub RunAsync()

       If Not (Me.IsDisposed) AndAlso Not (Me.Bgw.IsBusy) Then
           With Me.Bgw
               .WorkerSupportsCancellation = True
               .WorkerReportsProgress = True
               .RunWorkerAsync()
           End With

       Else
           Throw New InvalidOperationException("BackGroundWorker is already running.")

       End If

   End Sub

   <DebuggerStepThrough>
   Public Sub Pause()

       If Not (Me.IsDisposed) AndAlso Not (Me.isPausedB) Then
           Me.isPauseRequested = True
           Me.isPausedB = True
           Me.mreAsync.Reset()

       Else
           Throw New InvalidOperationException("BackGroundWorker is not running.")

       End If

   End Sub

   <DebuggerStepThrough>
   Public Sub [Resume]()

       If Not (Me.IsDisposed) AndAlso (Me.isPausedB) Then
           Me.isPausedB = False
           Me.isPauseRequested = False
           Me.mreAsync.Set()

       Else
           Throw New InvalidOperationException("BackGroundWorker is not paused.")

       End If

   End Sub

   <DebuggerStepThrough>
   Public Sub Cancel(Optional ByVal throwOnError As Boolean = False)

       Me.isCancelSyncRequested = True
       Me.CancelAsync()
       Me.mreSync.WaitOne()
       Me.isCancelSyncRequested = False

   End Sub

   <DebuggerStepThrough>
   Public Sub CancelAsync(Optional ByVal throwOnError As Boolean = False)

       If Not (Me.IsDisposed) AndAlso Not (Me.Bgw.CancellationPending) AndAlso (Me.Bgw.IsBusy) Then
           Me.mreAsync.Set() ' Resume thread if it is paused.
           Me.Bgw.CancelAsync()

       Else
           Throw New InvalidOperationException("BackGroundWorker is not initialized.")

       End If

   End Sub

#End Region

#Region " Private Methods "

   <DebuggerStepThrough>
   Private Function AddLvItem(ByVal src As ListView, ByVal dst As ListView, ByVal index As Integer) As ListViewItem

       Dim result As ListViewItem = Nothing

       If (dst.InvokeRequired) Then
           dst.Invoke(
               Sub()
                   Try
                       result = dst.Items.Add(src.Items(index).Text)
                   Catch ex As Exception
                       Debug.WriteLine(String.Format("Woker exception occured: {0}", ex.Message))
                   End Try
               End Sub)
       Else
           Try
               result = dst.Items.Add(src.Items(index).Text)
           Catch ex As Exception
               Debug.WriteLine(String.Format("Woker exception occured: {0}", ex.Message))
           End Try

       End If

       Return result

   End Function

#End Region

#Region " Event-Handlers "

   <DebuggerStepperBoundary>
   Private Sub MyWorker_DoWork(ByVal sender As Object, ByVal e As DoWorkEventArgs) _
   Handles Bgw.DoWork

       Dim lock As Object = ""
       SyncLock lock

           Dim lv1 As ListView = WindowsApplication1.Form1.ListView1
           Dim lv2 As ListView = WindowsApplication1.Form1.ListView2
           Dim max As Integer = lv2.Items.Count
           Dim itemIndex As Integer = -1

           For i As Integer = 0 To (max - 1)

               If (Me.Bgw.CancellationPending) Then
                   e.Cancel = True
                   Exit For

               Else
                   If Me.isPauseRequested Then ' Pause this thread.
                       Me.mreAsync.WaitOne(Timeout.Infinite)
                   End If

                   Dim lvi As ListViewItem = Me.AddLvItem(src:=lv2, dst:=lv1, index:=Interlocked.Increment(itemIndex))
                   If (lvi IsNot Nothing) Then
                       Debug.WriteLine(String.Format("ListViewItem Text: {0}", lvi.Text))
                   End If

                   If Me.Bgw.WorkerReportsProgress Then
                       Me.Bgw.ReportProgress((i + 1) * (100 \ max))
                   End If

                   Thread.Sleep(TimeSpan.FromSeconds(1))

               End If

           Next i

       End SyncLock

       If (Me.Bgw.WorkerReportsProgress) AndAlso Not (Me.Bgw.CancellationPending) Then
           Me.Bgw.ReportProgress(100)
       End If

       If (Me.isRunSync) OrElse (Me.isCancelSyncRequested) Then
           Me.mreSync.Set()
       End If

   End Sub

   <DebuggerStepperBoundary>
   Private Sub Bgw_ProgressChanged(ByVal sender As Object, ByVal e As ProgressChangedEventArgs) _
   Handles Bgw.ProgressChanged

       Debug.WriteLine(String.Format("Work Progress: {0:00.00}%", e.ProgressPercentage))

   End Sub

   <DebuggerStepperBoundary>
   Private Sub Bgw_RunWorkerCompleted(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs) _
   Handles Bgw.RunWorkerCompleted

       If (e.Cancelled) Then
           Debug.WriteLine("Work state: Cancelled")

       ElseIf (e.Error IsNot Nothing) Then
           Debug.WriteLine("Work state: Error")

       Else
           Debug.WriteLine("Work state: Success")

       End If

       Me.Dispose()

   End Sub

#End Region

#Region " IDisposable Implementation "

   Private isDisposedB As Boolean

   <DebuggerStepThrough>
   Public Sub Dispose() Implements IDisposable.Dispose

       Me.Dispose(isDisposing:=True)
       GC.SuppressFinalize(obj:=Me)

   End Sub

   <DebuggerStepThrough>
   Private Sub Dispose(ByVal isDisposing As Boolean)

       If (Not Me.isDisposedB) AndAlso (isDisposing) Then
           Me.Bgw.Dispose()
           Me.Bgw = Nothing

           With Me.mreSync
               .SafeWaitHandle.Close()
               .SafeWaitHandle.Dispose()
               .Close()
               .Dispose()
           End With

           With Me.mreAsync
               .SafeWaitHandle.Close()
               .SafeWaitHandle.Dispose()
               .Close()
               .Dispose()
           End With

       End If

       Me.isDisposedB = True

   End Sub

#End Region

#Region " Hidden Methods "

   <EditorBrowsable(EditorBrowsableState.Never)>
   <DebuggerNonUserCode>
   Public Shadows Function GetHashCode() As Integer
       Return MyBase.GetHashCode
   End Function

   <EditorBrowsable(EditorBrowsableState.Never)>
   <DebuggerNonUserCode>
   Public Shadows Function [GetType]() As Type
       Return MyBase.GetType
   End Function

   <EditorBrowsable(EditorBrowsableState.Never)>
   <DebuggerNonUserCode>
   Public Shadows Function Equals(ByVal obj As Object) As Boolean
       Return MyBase.Equals(obj)
   End Function

   <EditorBrowsable(EditorBrowsableState.Never)>
   <DebuggerNonUserCode>
   Public Shadows Function ToString() As String
       Return MyBase.ToString
   End Function

#End Region

End Class


Ejemplo de uso:
Código (vbnet) [Seleccionar]
Public NotInheritable Class Form1 : Inherits Form

   Friend MyWork As Work

   Private Sub Button1_Click() Handles Button1.Click

       If (Me.MyWork IsNot Nothing) Then
           Me.MyWork.Cancel()
       End If

       Me.MyWork = New Work
       Me.MyWork.RunAsync()

   End Sub

End Class


Como se puede ver, la clase donde encapsulo el BackGroundWorker tiene varias funcionalidades añadidas para controlar operaciones sincrónicas y asincrónicas, si quieres ver la implementación completa y original, y con la documentación XML, aquí lo tienes:


Saludos.








P4nd3m0n1um

#9
probe pero me da:

An exception of type 'System.InvalidOperationException' occurred in AutoLog.exe but was not handled in user code

Additional information: Error al crear el formulario. Consulte Exception.InnerException para obtener más detalles. Error: No se puede crear una instancia del control ActiveX '8856f961-340a-11d0-a96b-00c04fd705a2' porque el subproceso actual no está en un contenedor uniproceso.


En delphi no pasaba esto..  :-( jajaja