none
Los controles creados en un subproceso no pueden tener controles primarios en un control en un subproceso diferente.

    Pregunta

  • Saludos desde Tijuana, BC. Mexico.

    Acudo a este foro nuevamente para pedir amablemente de su colaboración y me puedan orientar en como solucionar mi problema.

    Resulta que estoy intentando de hacer algunas operaciones en un subproceso y el resultado final lo quiero mostrar en un CrystalReportViewer, pero cuando llega a este punto manda el error: Los controles creados en un subproceso no pueden tener controles primarios en un control en un subproceso diferente.

    Agradezco de antemano cualquier orientación que me puedan dar de como solucionar mi problema.

    A continuación les anexo el código.

     Private Sub Button3_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnGeneraCaratula.Click
            CrystalReportViewer1.ReportSource = Nothing
            btnGeneraCaratula.Enabled = False
            Label4.Visible = True
            barraProgreso.Visible = True
            barraProgreso.Style = ProgressBarStyle.Marquee
            barraProgreso.MarqueeAnimationSpeed = 100
            BackgroundWorker1.RunWorkerAsync()
        End Sub

    Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
            GeneradorCaratula_Click(sender)
        End Sub

           

    Sub GeneradorCaratula_Click(ByVal worker As System.ComponentModel.BackgroundWorker)
            CheckForIllegalCrossThreadCalls = False
            'Try
            'AQUI GENERA LOS IMPORTES DE LA COMPROBACION
            Label4.Text = "Generando importes de comprobación."

            If DataGridView1.ColumnCount > 0 Then
                DataGridView1.Columns.Clear()
            End If

            Dim cnn = New SqlConnection("data source = 192.168.0.69; initial catalog = BDControlViaticos; user id = sa; password = modatelas")
            cnn.Open()

            Dim cmd As New SqlCommand
            Dim reader As SqlDataReader

            cmd.CommandText = "SpTempImportesComprobados"
            cmd.CommandType = CommandType.StoredProcedure
            cmd.Connection = cnn

            cmd.Parameters.Add("@IdSemana", SqlDbType.Int).Value = ComboBox1.Text


            reader = cmd.ExecuteReader()
            reader.Close()


            Dim daA As New SqlDataAdapter("select * from TempImportesComprobados3", cnn)
            Dim dsA As New DataSet
            daA.Fill(dsA)
            DataGridView1.DataSource = dsA.Tables(0)
            DataGridView1.ClearSelection()

            cnn.Close()


            Dim suma As Double = 0
            For Each fila As DataGridViewRow In DataGridView1.Rows
                suma += CDbl(fila.Cells(2).Value)
            Next
            Dim UltimaFila As Integer = Me.DataGridView1.Rows.Count
            Me.DataGridView1.Rows(UltimaFila - 1).Cells(0).Value = "12"
            Me.DataGridView1.Rows(UltimaFila - 1).Cells(1).Value = "TOTAL COMPROBADO"
            Me.DataGridView1.Rows(UltimaFila - 1).Cells(2).Value = FormatCurrency(suma, 2)

            Dim suma1 As Double = 0
            For Each fila As DataGridViewRow In DataGridView1.Rows
                suma1 += CDbl(fila.Cells(3).Value)
            Next
            Dim UltimaFila1 As Integer = Me.DataGridView1.Rows.Count
            Me.DataGridView1.Rows(UltimaFila1 - 1).Cells(3).Value = FormatCurrency(suma1, 2)

            Dim suma2 As Double = 0
            For Each fila As DataGridViewRow In DataGridView1.Rows
                suma2 += CDbl(fila.Cells(4).Value)
            Next
            Dim UltimaFila2 As Integer = Me.DataGridView1.Rows.Count
            Me.DataGridView1.Rows(UltimaFila2 - 1).Cells(4).Value = FormatCurrency(suma2, 2)

            Dim suma3 As Double = 0
            For Each fila As DataGridViewRow In DataGridView1.Rows
                suma3 += CDbl(fila.Cells(5).Value)
            Next
            Dim UltimaFila3 As Integer = Me.DataGridView1.Rows.Count
            Me.DataGridView1.Rows(UltimaFila3 - 1).Cells(5).Value = FormatCurrency(suma3, 2)

            Dim suma4 As Double = 0
            For Each fila As DataGridViewRow In DataGridView1.Rows
                suma4 += CDbl(fila.Cells(6).Value)
            Next
            Dim UltimaFila4 As Integer = Me.DataGridView1.Rows.Count
            Me.DataGridView1.Rows(UltimaFila4 - 1).Cells(6).Value = FormatCurrency(suma4, 2)

            Dim suma5 As Double = 0
            For Each fila As DataGridViewRow In DataGridView1.Rows
                suma5 += CDbl(fila.Cells(7).Value)
            Next
            Dim UltimaFila5 As Integer = Me.DataGridView1.Rows.Count
            Me.DataGridView1.Rows(UltimaFila5 - 1).Cells(7).Value = FormatCurrency(suma5, 2)

            Dim suma6 As Double = 0
            For Each fila As DataGridViewRow In DataGridView1.Rows
                suma6 += CDbl(fila.Cells(8).Value)
            Next
            Dim UltimaFila6 As Integer = Me.DataGridView1.Rows.Count
            Me.DataGridView1.Rows(UltimaFila6 - 1).Cells(8).Value = FormatCurrency(suma6, 2)

            For i = 1 To 1
                Me.DataGridView1.Columns.Add("TOTAL", "TOTAL")
            Next

            For Each row As DataGridViewRow In DataGridView1.Rows
                Dim Dia1 As Double = CStr(row.Cells(2).Value)
                Dim Dia2 As Double = CStr(row.Cells(3).Value)
                Dim Dia3 As Double = CStr(row.Cells(4).Value)
                Dim Dia4 As Double = CStr(row.Cells(5).Value)
                Dim Dia5 As Double = CStr(row.Cells(6).Value)
                Dim Dia6 As Double = CStr(row.Cells(7).Value)
                Dim Dia7 As Double = CStr(row.Cells(8).Value)
                row.Cells(9).Value = FormatCurrency(Dia1 + Dia2 + Dia3 + Dia4 + Dia5 + Dia6 + Dia7, 2)
            Next

            GridAExcel(DataGridView1)


            'AQUI INSERTA LOS IMPORTES GENERADOS EN LA BASE DE DATOS
            Label4.Text = "Insertando importes en la BD."

            'Dim cnn = New SqlConnection("data source = 192.168.0.69; initial catalog = BDControlViaticos; user id = sa; password = modatelas")
            cnn.Open()

            Dim buscartabla As New SqlCommand
            Dim readerbd As SqlDataReader
            buscartabla.CommandText = "select * from BDControlViaticos.dbo.sysobjects where name='TempTotalizacionComprobacion'"
            buscartabla.CommandType = CommandType.Text
            buscartabla.Connection = cnn
            readerbd = buscartabla.ExecuteReader()
            If readerbd.Read Then
                Dim strSQL As String = _
                "DROP TABLE TempTotalizacionComprobacion;"
                Dim EliminarTabla As New SqlCommand(strSQL, cnn)
                EliminarTabla.CommandType = CommandType.Text

                readerbd.Close()
                EliminarTabla.ExecuteNonQuery()

                EliminarTabla.Dispose()
                readerbd.Close()
            End If
            cnn.Close()

            Using conn As New OleDbConnection( _
                    "Provider=Microsoft.ACE.OLEDB.12.0;" & _
                    "Data Source=C:\Users\" & UsuarioTxt.Text & "\Documents\TempTotalizacionComprobacion.xlsx;" & _
                    "Extended Properties='Excel 12.0 Xml;HDR=Yes'")

                ' Importamos los datos utilizando una cadena ODBC
                Dim sql As String = _
                    "SELECT * INTO [ODBC;Driver={SQL Server};" & _
                    "Server=192.168.0.69;Database=BDControlViaticos;" & _
                    "UID=sa;PWD=modatelas].TempTotalizacionComprobacion " & _
                    "FROM [TempTotalizacionComprobacion$]"

                Dim cmdSQL As New OleDbCommand(sql, conn)
                conn.Open()
                Dim n As Int32 = cmdSQL.ExecuteNonQuery
            End Using

            If File.Exists("C:\Users\" & UsuarioTxt.Text & "\Documents\TempTotalizacionComprobacion.xlsx") Then
                File.Delete("C:\Users\" & UsuarioTxt.Text & "\Documents\TempTotalizacionComprobacion.xlsx")
            End If


            'AQUI GENERA LA CARATULA
            Label4.Text = "Generando caratula."
            'Dim cnn = New SqlConnection("data source = 192.168.0.69; initial catalog = BDControlViaticos; user id = sa; password = modatelas")
            cnn.Open()

            Dim Spcmd As New SqlCommand
            Dim SpReader As SqlDataReader

            Spcmd.CommandText = "SpGenerarCaratulaComprobaciones"
            Spcmd.CommandType = CommandType.StoredProcedure
            Spcmd.Connection = cnn

            Spcmd.Parameters.Add("@IdSemana", SqlDbType.Int).Value = ComboBox1.Text
            Spcmd.Parameters.Add("@EmpleadoNo", SqlDbType.NVarChar).Value = ComboBox2.Text

            SpReader = Spcmd.ExecuteReader()
            SpReader.Close()       

    ConsultarCaratulaComprobacionCrystalReport()

            cnn.Close()
            'Catch ex As Exception
            barraProgreso.Value = 100
            '    MsgBox(ex.Message, MsgBoxStyle.Critical)
            'End Try
        End Sub


     Sub ConsultarCaratulaComprobacionCrystalReport()
            Dim tabla As String = "GenerarCaratulaComprobaciones"
            Dim SqlString As String = "select * from GenerarCaratulaComprobaciones"
            Dim ds As DataSet = CargarDatasetGenerarCaratulaComprobaciones(SqlString, tabla)

            Dim rpt As New CrystalReportConsultarCaratulaComprobacion
            rpt.SetDataSource(ds)
            CrystalReportViewer1.ReportSource = rpt   '<---Justamente aqui es donde salta el error
        End Sub

    Public Function CargarDatasetGenerarCaratulaComprobaciones(ByVal SqlString As String, ByVal tabla As String) As DataSet
            Try
                Dim cnn = New SqlConnection("data source = 192.168.0.69; initial catalog = BDControlViaticos; user id = sa; password = modatelas")
                'Dim cnn = New SqlConnection("data source = 192.168.0.69; initial catalog = BDControlViaticos; user id = sa; password = modatelas")
                Dim SQLdataset As New DataSet
                Dim Sqldapter As New SqlDataAdapter
                cnn.Open()
                Sqldapter = New SqlDataAdapter(SqlString, cnn)
                Sqldapter.Fill(SQLdataset, tabla)
                Return SQLdataset
                cnn.Close()
            Catch ex As Exception
                barraProgreso.Value = 100
                Return Nothing
                MessageBox.Show("No se pudo completar la operación, intente de verificar la conexión a la base de datos.", "Fallo conexión a la base de datos", MessageBoxButtons.OK, MessageBoxIcon.Error)
            End Try
        End Function

    Nuevamente, agradezco su ayuda.

    jueves, 24 de noviembre de 2016 3:47

Respuestas

  • Estuve analizando lo que me sugieres pero no se me ocurre como hacerlo. Crees que puedas diluirme un poco mas lo que comentas.

    Tienes que escribir DOS subrutinas en lugar de una. Toma tu subrutina Setcrystal y sepárala en dos que sean SetcrystalObtenerDatos y SetcrystalPresentarDatos. En la primera haces las operaciones costosas, en tu caso concreto la parte que hace la llamada a CargarDatasetGenerarCaratulaComprobaciones y guarda los resultados en ds. Y en la segunda subrutina metes la parte en la que construyes el rpt y le asignas el ds en el reportsource. Evidentemente, la variable ds tienes que pasarla de una rutina a la otra; puedes usar una variable global a nivel de la clase que contiene las dos rutinas, si es que solo vas a tener en curso un único hilo haciendo esta operación. Si vas a hacer más de una en paralelo, tendrás que pasar el ds a través del Invoke, cosa que tiene una cierta complejidad pero creo que no será tu caso.

    En la subrutina ConsultarCaratulaComprobacionCrystalReport, en lugar de llamar a SetCrystal ahora llamarás a SetcrystalObtenerDatos. Y al final de SetcrystalObtenerDatos usas el Invoke para llamar a SetcrystalPresentarDatos.

    En cuanto a las otras cosas que te ocurren con la aplicación, las causas no son evidentes a la vista del código presentado, debe de influir alguna secuencia de eventos que ahí no se ven y que lanzan alguna operación al maximizar o al hacer las consultas. Utiliza el debugger de visual studio y ejecuta la aplicación paso a paso viendo por dónde se mete en cada momento, hasta que localices el sitio exacto en el que "pierde tiempo" durante este proceso.

    • Marcado como respuesta delfino morales lunes, 28 de noviembre de 2016 1:52
    domingo, 27 de noviembre de 2016 9:52
  • Dime si asi es como me lo sugerias

    Sí, eso es lo que yo decía. El Invoke requiere un parámetro del tipo MethodInvoker, que es un delegado que apunta a una subrutina que no recibe ningún argumento. En teoría podrías emular el paso de un parámetro mediante un closure usando una lambda, pero es más sencillo usar una variable de clase como has hecho en el último ejemplo.
    • Marcado como respuesta delfino morales lunes, 28 de noviembre de 2016 20:31
    lunes, 28 de noviembre de 2016 8:14

Todas las respuestas

  • Observo que en el programa has puesto CheckForIllegalCrossThreadCalls = False. Esto es un disparate. No lo uses jamás. Lo que hace es que te deshabilita las comprobaciones que tiene internamente el sistema para que desde un hilo distinto no puedas llamar a los controles de pantalla. Esto es importante porque la interfaz de usuario no es segura para uso en multihilo. Llamar a los controles de pantalla desde otro hilo provocará fallos intermitentes dificilísimos de depurar; tu programa fallará de vez en cuando sin que sepas por qué.

    Es importante dejar el CheckForIllegalCrossThreadCalls en su valor predeterminado True para que se produzca un error si por casualidad se te ha escapado sin darte cuenta en el hilo de background algún acceso a la pantalla. De esa forma, lo detectas y puedes suprimirlo. Esto debe ser lo que te está pasando con el Crystal Reports, que al fin y al cabo es un control que accede a la interfaz de usuario, y por lo tanto no es lícito llamarlo desde otro hilo.

    En general, siempre que lances un subproceso en otro hilo, el subproceso solo debe hacer cálculos y accesos a bases de datos u otros objetos externos, tales como archivos. Y cuando finalmente obtenga los resultados, no los debe presentar en pantalla (de ninguna manera, ni siquiera con un reporte). Lo que tiene que hacer es dejar esos resultados en una variable en memoria, y luego devolver el control al hilo principal (usando Invoke o BeginInvoke), y una vez en el hilo principal entonces ya se trasladan desde esa variable a la pantalla.

    jueves, 24 de noviembre de 2016 20:31
  • Gracias Alberto por tomarte la molestia en responder.

    Tomare en cuenta lo que me dices, e investigare como utilizar Invoke o BeginInvoke.

    Saludos.

    jueves, 24 de noviembre de 2016 23:56
  • Hola buenas tardes,

    Alberto realice lo que comentabas con Invoke, y realiza el proceso completo y me muestra el resultado, pero al finalizar me deja congelado el formulario por 20 segundos aproximadamente. 

    ¿Que estare haciendo mal?

    Te anexo el listado:


        Private dt As Thread = Nothing

        Delegate Sub crystal(ByVal report As CrystalReportConsultarCaratulaComprobacion)

    • Estoy llamando así el método.

    dt = New Thread( _
                New ThreadStart(AddressOf ConsultarCaratulaComprobacionCrystalReport))
                dt.Start()

    • El metodo

    Sub ConsultarCaratulaComprobacionCrystalReport()
            Setcrystal(CrystalReportViewer1.ReportSource)
        End Sub

     Private Sub Setcrystal(ByVal [report] As CrystalReportConsultarCaratulaComprobacion)
            Dim tabla As String = "GenerarCaratulaComprobaciones"
            Dim SqlString As String = "select * from GenerarCaratulaComprobaciones"
            Dim ds As DataSet = CargarDatasetGenerarCaratulaComprobaciones(SqlString, tabla)

            Dim rpt As New CrystalReportConsultarCaratulaComprobacion
            rpt.SetDataSource(ds)
            'CrystalReportViewer1.ReportSource = rpt

            If Me.CrystalReportViewer1.InvokeRequired Then
                Dim d As New crystal(AddressOf Setcrystal)
                Me.Invoke(d, New CrystalReportConsultarCaratulaComprobacion() {[rpt]})
            Else
                Me.CrystalReportViewer1.ReportSource = [rpt]
            End If
        End Sub

    viernes, 25 de noviembre de 2016 2:37
  • En el Invoke estás llamando de nuevo al mismo método que dentro tiene TODO el cálculo (no solo la presentación de resultados). Esto hace que se repita de nuevo el mismo cálculo que ya habías hecho antes, pero esta vez dentro del hilo principal, con lo que te congela el formulario mientras lo está realizando.

    Sepáralo en dos, y llama desde el Thread a la parte que hace CargarDatasetGenerarCaratulaComprobaciones, y cuando termine haz un Invoke que llame a otra subrutina que dentro tenga todas las llamadas al CrystalReportViewer1.

    viernes, 25 de noviembre de 2016 8:09
  • Buenas noches, disculpa la tardanza en responder.

    Estuve analizando lo que me sugieres pero no se me ocurre como hacerlo. Crees que puedas diluirme un poco mas lo que comentas.

    Aprovecho para comentar tambien que la aplicacion se congela cuando quiero maximizarla, y esto solo ocurre la primera vez que abro el formulario. Si yo no cierro dicho formulacion y hago una segunda consulta ya no se me congela para nada. 

    No se si me explico pero esto solo ocurre la primera vez que hago la consulta y mientras no la cierre las demas consultas no generan ese conflicto.

    Saludos, y muchas gracias por tu tiempo.

    domingo, 27 de noviembre de 2016 4:09
  • Estuve analizando lo que me sugieres pero no se me ocurre como hacerlo. Crees que puedas diluirme un poco mas lo que comentas.

    Tienes que escribir DOS subrutinas en lugar de una. Toma tu subrutina Setcrystal y sepárala en dos que sean SetcrystalObtenerDatos y SetcrystalPresentarDatos. En la primera haces las operaciones costosas, en tu caso concreto la parte que hace la llamada a CargarDatasetGenerarCaratulaComprobaciones y guarda los resultados en ds. Y en la segunda subrutina metes la parte en la que construyes el rpt y le asignas el ds en el reportsource. Evidentemente, la variable ds tienes que pasarla de una rutina a la otra; puedes usar una variable global a nivel de la clase que contiene las dos rutinas, si es que solo vas a tener en curso un único hilo haciendo esta operación. Si vas a hacer más de una en paralelo, tendrás que pasar el ds a través del Invoke, cosa que tiene una cierta complejidad pero creo que no será tu caso.

    En la subrutina ConsultarCaratulaComprobacionCrystalReport, en lugar de llamar a SetCrystal ahora llamarás a SetcrystalObtenerDatos. Y al final de SetcrystalObtenerDatos usas el Invoke para llamar a SetcrystalPresentarDatos.

    En cuanto a las otras cosas que te ocurren con la aplicación, las causas no son evidentes a la vista del código presentado, debe de influir alguna secuencia de eventos que ahí no se ven y que lanzan alguna operación al maximizar o al hacer las consultas. Utiliza el debugger de visual studio y ejecuta la aplicación paso a paso viendo por dónde se mete en cada momento, hasta que localices el sitio exacto en el que "pierde tiempo" durante este proceso.

    • Marcado como respuesta delfino morales lunes, 28 de noviembre de 2016 1:52
    domingo, 27 de noviembre de 2016 9:52
  • Saludos,

    Deacuerdo a lo que dices,... Seria algo asi?

    Se que en algo etoy mal, ya que no me actualiza el reporte. Pero no logro identificar en donde tengo el error.

    Dim ds As New DataSet

    Sub SetcrystalObtenerDatos()
            'Try
            Dim tabla As String = "GenerarCaratulaComprobaciones"
            Dim SqlString As String = "select * from GenerarCaratulaComprobaciones"
            Dim cnn = New SqlConnection("data source = 192.168.0.69; initial catalog = BDControlViaticos; user id = sa; password = modatelas")
            Dim Sqldapter As New SqlDataAdapter
            cnn.Open()
            Sqldapter = New SqlDataAdapter(SqlString, cnn)
            Sqldapter.Fill(ds, tabla)
            Invoke(Sub() SetcrystalPresentarDatos(ds))
            'Catch ex As Exception
            barraProgreso.Value = 100
            'MessageBox.Show("No se pudo completar la operación, intente de verificar la conexión a la base de datos.", "Fallo conexión a la base de datos", MessageBoxButtons.OK, MessageBoxIcon.Error)
            'End Try
        End Sub

        Sub SetcrystalPresentarDatos(ds As DataSet)        
            Dim rpt As New CrystalReportConsultarCaratulaComprobacion
            rpt.SetDataSource(ds)
            CrystalReportViewer1.ReportSource = rpt
        End Sub


    • Editado delfino morales domingo, 27 de noviembre de 2016 22:14 Me fato agregar mas informacion.
    domingo, 27 de noviembre de 2016 22:13
  • Hola, ya encontré mi error.

    Solo Cambie la manera en que declaro la variable, quedando de la siguiente manera.

    Dim ds As DataSet

    Sub SetcrystalObtenerDatos()
            'Try
            Dim tabla As String = "GenerarCaratulaComprobaciones"
            Dim SqlString As String = "select * from GenerarCaratulaComprobaciones"
            Dim cnn = New SqlConnection("data source = 192.168.0.69; initial catalog = BDControlViaticos; user id = sa; password = modatelas")
            Dim Sqldapter As New SqlDataAdapter
            cnn.Open()
            Sqldapter = New SqlDataAdapter(SqlString, cnn)

            ds = New DataSet
            Sqldapter.Fill(ds, tabla)

            Invoke(Sub() SetcrystalPresentarDatos())
            'Catch ex As Exception
            barraProgreso.Value = 100
            'MessageBox.Show("No se pudo completar la operación, intente de verificar la conexión a la base de datos.", "Fallo conexión a la base de datos", MessageBoxButtons.OK, MessageBoxIcon.Error)
            'End Try
        End Sub

        Sub SetcrystalPresentarDatos()        
            Dim rpt As New CrystalReportConsultarCaratulaComprobacion
            rpt.SetDataSource(ds)
            CrystalReportViewer1.ReportSource = rpt
        End Sub

    Con esto realiza la consulta sin problema. Dime si asi es como me lo sugerias.



    lunes, 28 de noviembre de 2016 1:38
  • Y sobre lo de que la aplicacion se congela por unos segundo ya vi que es el visor de crystal el que ocaciona eso.... are lo que mencionas d eejecutar paso a paso para identificar donde tengo el problema.
    lunes, 28 de noviembre de 2016 1:54
  • Dime si asi es como me lo sugerias

    Sí, eso es lo que yo decía. El Invoke requiere un parámetro del tipo MethodInvoker, que es un delegado que apunta a una subrutina que no recibe ningún argumento. En teoría podrías emular el paso de un parámetro mediante un closure usando una lambda, pero es más sencillo usar una variable de clase como has hecho en el último ejemplo.
    • Marcado como respuesta delfino morales lunes, 28 de noviembre de 2016 20:31
    lunes, 28 de noviembre de 2016 8:14
  • Dime si asi es como me lo sugerias

    Sí, eso es lo que yo decía. El Invoke requiere un parámetro del tipo MethodInvoker, que es un delegado que apunta a una subrutina que no recibe ningún argumento. En teoría podrías emular el paso de un parámetro mediante un closure usando una lambda, pero es más sencillo usar una variable de clase como has hecho en el último ejemplo.

    Muchas gracias por aportar compartir tu conocimiento, y por el tiempo que te tomaste en responder.

    Que este muy bien, Saludos.

    lunes, 28 de noviembre de 2016 20:34