none
Cargar ficheros CSV demasiado "pesados" RRS feed

  • Pregunta

  • Buenas a todos, 

    Para una determinada aplicación estoy realizando la lectura de unos datos de un fichero CSV, una vez leídos necesito darle un formato específico para cargar estos datos en una dll. Todo funciona correctamente salvo cuando el fichero CSV contiene gran cantidad de datos, que el programa se realentiza mucho, no al cargar los datos, sino al cambiar el formato. El código que estoy empleando es el siguiente: 

     string Value_1="";
     string Value_2 = "";
     string Value_3 = "";
     string Tiempo = "";

               for (int i = 0; i < filas-1; i++)
                {
    Value_1 = Value_1 + Convert.ToString(Values.Rows[i].ItemArray[4]).Replace(".", ",")+";";
    Value_2 = Value_2 + Convert.ToString(Values.Rows[i].ItemArray[5]).Replace(".", ",") + ";";
    Value_3 = Value_3 + Convert.ToString(Values.Rows[i].ItemArray[6]).Replace(".", ",") + ";";
    Tiempo = Tiempo + Convert.ToString(Values.Rows[i].ItemArray[0]).Replace(".", ",") + ";";
                }

    Sabrían decirme cómo puedo solucionar este problema de forma que el proceso no se me quede colgado? El número de filas, está en torno a 250.000. 

    Gracias de antemano. 

    Un saludo

                                                   

    lunes, 18 de junio de 2018 8:01

Respuestas

  • Buenas,

    Estas usando un TabControl entiendo, si es asi, tienes una propiedad que se llama  TabCount , con ella puedes saber el numero de pestañas abiertas que tiene el control, cuando quieran abrir una nueva, evaluar si ese valor es menor o igual que "n", y si no cumple, sacar un mensaje diciendo que antes de crear una nueva se debe cerrar alguna, o incluso, cerrarla tu, quitar la que tenga indice 0 (la más antigua), de modo que siempre tengas un número finito de pestañas.

    Respondiendo al tema del evento de la segunda opción, puedes usar varios eventos:  OnDeselected(TabControlEventArgs)/OnDeselecting(TabControlEventArgs) para saber cual se deselecciona y OnSelected(TabControlEventArgs)/OnSelecting(TabControlEventArgs) para saber cual se selecciona, tambien puedes gestionarlo entero tu con SelectedIndezChanged, que te dice el indice da tab que se ha seleccionado.

    Atte


    Jorge Turrado Ferrero

    Mis repositorios en GitHub

    No olvides votar mi comentario si te ha ayudado y marcarlo como respuesta si ha sido la solución, con eso ayudas a mejorar mi reputación en la comunidad y a identificar la respuesta a la gente que tenga el mismo problema.

    Para obtener una respuesta lo más rápida y concisa posible, te recomiendo:

    • Marcado como respuesta GabrieloBurja miércoles, 27 de junio de 2018 13:34
    miércoles, 27 de junio de 2018 10:28

Todas las respuestas

  • Buenas, 

    Seguramente se quede colgado porque lo haces en el mismo thread principal, en este caso, tu aplicación hace un uso intensivo que necesita tiempo, lo ideal seria que ese proceso lo hicieras en un thread aparte, y que notificases que has terminado mediante algun sistema estilo ManualResetEvent o AutoResetEvent, o incluso usaras asincronismos para liberar al main de esperar a que acabe la ejecucion.

    Si estas interesado en que miremos de hacerlo asi (a mi modo de ver la manera mas limpia), comentanos y lo encaramos con multiproceso.

    Una manera, aunque a mi no me gusta mucho, que haria que tu aplicacion no se ralentice, es añadir la linea Application.DoEvents() en cada iteracion, con eso, el formulario no se va a congelar, lo cual puede que en tu caso sea suficiente...

    string Value_1="";
    string Value_2 = "";
    string Value_3 = "";
    string Tiempo = "";
    for (int i = 0; i < filas-1; i++)
    {
        Value_1 = Value_1 + Convert.ToString(Values.Rows[i].ItemArray[4]).Replace(".", ",")+";"; 
        Value_2 = Value_2 + Convert.ToString(Values.Rows[i].ItemArray[5]).Replace(".", ",") + ";"; 
        Value_3 = Value_3 + Convert.ToString(Values.Rows[i].ItemArray[6]).Replace(".", ",") + ";";
        Tiempo = Tiempo + Convert.ToString(Values.Rows[i].ItemArray[0]).Replace(".", ",") + ";";
        Application.DoEvents();
    }

    Quedo a la espera de tu respuesta

    Atte


    Jorge Turrado Ferrero

    Mis repositorios en GitHub

    No olvides votar mi comentario si te ha ayudado y marcarlo como respuesta si ha sido la solución, con eso ayudas a mejorar mi reputación en la comunidad y a identificar la respuesta a la gente que tenga el mismo problema.

    Para obtener una respuesta lo más rápida y concisa posible, te recomiendo:

    lunes, 18 de junio de 2018 8:08
  • Lo primero es que hay unas mejoras básicas que tienes que hacer a tu código, no uses string ya que es inmutable y crea nuevas instancias cada vez, en su lugar usa stringbuilder, tambien puedes pasar el replace al final, como ejemplo sería algo así:

    StringBuilder sbValue_1 = new StringBuilder();
    for (int i = 0; i < filas - 1; i++)
    {
    sbValue_1.Append(Values.Rows[i].ItemArray[4].ToString()) + ";";
    }
    sbValue_1.Replace(".", ",")


    lunes, 18 de junio de 2018 10:24
  • Buenas Jorge, gracias. 

    Efectivamente, ahora el proceso no se queda "enganchado", el problema es el tiempo de ejecución estamos en torno a los 10 minutos de cómputo. Puedes indicarme algunas nociones de lo que has comentado mediante el theard aparte. 

    Gracias de antemano. 

    lunes, 18 de junio de 2018 14:53
  • Buenas Anibal, voy a probar también lo que comentas a ver si me mejora considerablemente el tiempo de cálculo. 

    Gracias. 

    lunes, 18 de junio de 2018 14:54
  • Claro,

    Si colocas el código del formulario lo hacemos sin problema para que quede bien

    Atte


    Jorge Turrado Ferrero

    Mis repositorios en GitHub

    No olvides votar mi comentario si te ha ayudado y marcarlo como respuesta si ha sido la solución, con eso ayudas a mejorar mi reputación en la comunidad y a identificar la respuesta a la gente que tenga el mismo problema.

    Para obtener una respuesta lo más rápida y concisa posible, te recomiendo:

    lunes, 18 de junio de 2018 14:59
  • Buenas Jorge, disculpa he andado liado con el fin de curso... te mando un esquema del load que al final es donde hago la carga de datos y representación de los mismos y es donde tengo problemas. Al final he modificado la dll para intentar quitar "tareas" de este load. El registro de datos lo hago de un datalogger capaz de medir temperatura, humedad, presión y los datos me los proporciona en unos ficheros CSV. A partir de hay lo que hago es pasar esos a una dll en la que hago un filtrado de las diferentes señales, este filtrado la paso a una dll hecha en otro software con más potencia matemática y con la que tengo más práctica. Te paso el código de la función load... 

     private void AnálisisTest_Load(object sender, EventArgs e)
    {
      FormResizer objFormResizer = new FormResizer();
      objFormResizer.ResizeForm(this, 1080, 1920);

    string path = Directory.GetCurrentDirectory() + "\\Datos Adquiridos\\" + "" + txtAnalisisFile.Text+"\\"; 

    //En path tengo un string con la ruta donde tengo los ficheros CSV del datalogger

     CMathLV.DevelopDLLV2(pathcompleto);

    //esta es la dll que he desarrollado con otro software aquí lo que hago es filtrar la señal y realizar ciertos cálculos como derivadas y preparar otro fichero CSV con todos los datos tratados. La entrada de la dll por lo tanto es una ruta y la salida al terminar un fichero csv, que es el que deseo leer y cargar en graficas en la aplicación de VS. 

    string pathDatos = Directory.GetCurrentDirectory() + "\\Datos Adquiridos\\" + "\\Temporal Data.csv";

    DataTable TotalData = new DataTable();
    TotalData = ReadCSV(pathDatos);

    //a partir de hay, representamos...

                    chtGrf_Temp.DataSource = TotalData ;
                    chtGrf_Temp.Series[0].Name = "Temp_S1";
                    chtGrf_Temp.Series[0].XValueMember = "Tiempo(s)";
                    chtGrf_Temp.Series[0].YValueMembers = "T_sonda1";
                    chtGrf_Temp.Series[1].Name = "Temp_S2";
                    chtGrf_Temp.Series[1].XValueMember = "Tiempo(s)";
                    chtGrf_Temp.Series[1].YValueMembers = "T_sonda2";
                    chtGrf_Temp.Series[2].Name = ""Temp_S3";
                    chtGrf_Temp.Series[2].XValueMember = "Tiempo(s)";
                    chtGrf_Temp.Series[2].YValueMembers = "T_sonda3";
                    chtGrf_Temp.ChartAreas[0].AxisX.Interval = 150;
                    chtGrf_Temp.ChartAreas[0].CursorX.IsUserSelectionEnabled = true;

    chtGrf_Hr.DataSource = TotalData;
                    chtGrf_Hr.Series[0].Name = "Hr_S1";
                    chtGrf_Hr.Series[0].XValueMember = "Tiempo(s)";
                    chtGrf_Hr.Series[0].YValueMembers = "Hr_S1";
                    chtGrf_Hr.Series[1].Name = "Hr_S2";
                    chtGrf_Hr.Series[1].XValueMember = "Tiempo(s)";
                    chtGrf_Hr.Series[1].YValueMembers = "Hr_S2";
                    chtGrf_Hr.Series[2].Name = "Hr_S3";
                    chtGrf_Hr.Series[2].XValueMember = "Tiempo(s)";
                    chtGrf_Hr.Series[2].YValueMembers = "Hr_S3";
                    chtGrf_Hr.ChartAreas[0].AxisX.Interval = 1;
                    chtGrf_Hr.ChartAreas[0].CursorX.IsUserSelectionEnabled = true;

    chtGrf_TempFilt.DataSource = TotalData ;
                    chtGrf_TempFilt.Series[0].Name = "TempFilt_S1";
                    chtGrf_TempFilt.Series[0].XValueMember = "Tiempo(s)";
                    chtGrf_TempFilt.Series[0].YValueMembers = "TFilt_sonda1";
                    chtGrf_TempFilt.Series[1].Name = "TempFilt_S2";
                    chtGrf_TempFilt.Series[1].XValueMember = "Tiempo(s)";
                    chtGrf_TempFilt.Series[1].YValueMembers = "TFilt_sonda2";
                    chtGrf_TempFilt.Series[2].Name = ""TempFilt_S3";
                    chtGrf_TempFilt.Series[2].XValueMember = "Tiempo(s)";
                    chtGrf_TempFilt.Series[2].YValueMembers = "TFilt_sonda3";
                    chtGrf_TempFilt.ChartAreas[0].AxisX.Interval = 150;
                    chtGrf_TempFilt.ChartAreas[0].CursorX.IsUserSelectionEnabled = true;

     }

    public DataTable ReadCSV (string filename)
            {
                DataTable dt = new DataTable("Valores");
                string [] raw_text = System.IO.File.ReadAllLines(filename);
                string[] data_col = null;
                int x = 0;
                foreach (string text_line in raw_text)
                {
                  
                    data_col = text_line.Split(',');
                    if (x == 0)
                    {
                        for (int i = 0; i <= data_col.Count() - 1; i++)
                        {
                            dt.Columns.Add(data_col [i]);
                        }
                        x++;
                    }
                    else
                    {
                       
                        dt.Rows.Add(data_col);
                    }
                }

    }

    Te he añadido la función de lectura de CSV que gasto. 

    Cualquier mejora o sugerencia se agradece, como comenté en el primer mensaje, si los datos son pocos, minutos no existe problema, ahora cuando nos vamos a datos adquiridos durante mucho tiempo 20 min el software no es capaz de cargar los datos. 

    Por cierto, verás que con la nueva dll, no tengo que hacer el cambio de "." por "," ni acondicionar los datos de entrada a un string, pues directamente lo hago en la dll es pasar el fichero de datos y la dll la que se encarga de hacer ese procesado. Ahora bien, me gustaría no tener problemas de compatibilidad de los datos con la configuración regional del equipo, cómo se puede resolver esto? 

    Gracias de nuevo. 


    viernes, 22 de junio de 2018 9:49
  • Buenas,

    La verdad es que no veo el código que te daba guerra antes, para ese que pones, se me ocurre que puedes hacer todo el trabajo en un thread, de modo que Load se ejecute bien, y mediante invocaciones mostrar los resultados en los controles cuando ya estén generados. La opción de usar Parallel.Foreach(....) entiendo que no es valida porque las filas del fichero entiendo que van ordenadas.

    Te dejo una posible modificación al código a ver que te parece:

    private void AnálisisTest_Load(object sender, EventArgs e)
    {
      FormResizer objFormResizer = new FormResizer();
      objFormResizer.ResizeForm(this, 1080, 1920);
    
      //Iniciamos un thread que haga todo lo de tu metodo Load
      new thread(()=>
      {
    	string path = Directory.GetCurrentDirectory() + "\\Datos Adquiridos\\" + "" + txtAnalisisFile.Text+"\\"; 
    
    	//En path tengo un string con la ruta donde tengo los ficheros CSV del datalogger
    
    	CMathLV.DevelopDLLV2(pathcompleto);
    
    	//esta es la dll que he desarrollado con otro software aquí lo que hago es filtrar la señal y realizar ciertos cálculos como derivadas y preparar otro fichero CSV con todos los datos tratados. La entrada de la dll por lo tanto es una ruta y la salida al terminar un fichero csv, que es el que deseo leer y cargar en graficas en la aplicación de VS. 
    
    	string pathDatos = Directory.GetCurrentDirectory() + "\\Datos Adquiridos\\" + "\\Temporal Data.csv";
    
    	DataTable TotalData = new DataTable();
    	TotalData = ReadCSV(pathDatos);
    
    	//a partir de hay, representamos...
    	
    	//Como no se puede acceder a un control desde un hilo diferente al que lo crea, hacemos una invocacion para hacer llamadas seguras
        chtGrf_Temp.BeginInvoke(new MethodInvoker(()=>{
    		chtGrf_Temp.DataSource = TotalData ;
    		chtGrf_Temp.Series[0].Name = "Temp_S1";
    		chtGrf_Temp.Series[0].XValueMember = "Tiempo(s)";
    		chtGrf_Temp.Series[0].YValueMembers = "T_sonda1";
    		chtGrf_Temp.Series[1].Name = "Temp_S2";
    		chtGrf_Temp.Series[1].XValueMember = "Tiempo(s)";
    		chtGrf_Temp.Series[1].YValueMembers = "T_sonda2";
    		chtGrf_Temp.Series[2].Name = "Temp_S3";
    		chtGrf_Temp.Series[2].XValueMember = "Tiempo(s)";
    		chtGrf_Temp.Series[2].YValueMembers = "T_sonda3";
    		chtGrf_Temp.ChartAreas[0].AxisX.Interval = 150;
    		chtGrf_Temp.ChartAreas[0].CursorX.IsUserSelectionEnabled = true;
    	}));	
    	chtGrf_Hr.BeginInvoke(new MethodInvoker(()=>{
    		chtGrf_Hr.DataSource = TotalData;
    		chtGrf_Hr.Series[0].Name = "Hr_S1";
    		chtGrf_Hr.Series[0].XValueMember = "Tiempo(s)";
    		chtGrf_Hr.Series[0].YValueMembers = "Hr_S1";
    		chtGrf_Hr.Series[1].Name = "Hr_S2";
    		chtGrf_Hr.Series[1].XValueMember = "Tiempo(s)";
    		chtGrf_Hr.Series[1].YValueMembers = "Hr_S2";
    		chtGrf_Hr.Series[2].Name = "Hr_S3";
    		chtGrf_Hr.Series[2].XValueMember = "Tiempo(s)"; 
    		chtGrf_Hr.Series[2].YValueMembers = "Hr_S3";
    		chtGrf_Hr.ChartAreas[0].AxisX.Interval = 1;
    		chtGrf_Hr.ChartAreas[0].CursorX.IsUserSelectionEnabled = true;
    		}));	
    	chtGrf_TempFilt.BeginInvoke(new MethodInvoker(()=>{
    		chtGrf_TempFilt.DataSource = TotalData ;
    		chtGrf_TempFilt.Series[0].Name = "TempFilt_S1";
    		chtGrf_TempFilt.Series[0].XValueMember = "Tiempo(s)";
    		chtGrf_TempFilt.Series[0].YValueMembers = "TFilt_sonda1";
    		chtGrf_TempFilt.Series[1].Name = "TempFilt_S2";
    		chtGrf_TempFilt.Series[1].XValueMember = "Tiempo(s)";
    		chtGrf_TempFilt.Series[1].YValueMembers = "TFilt_sonda2";
    		chtGrf_TempFilt.Series[2].Name = "TempFilt_S3";
    		chtGrf_TempFilt.Series[2].XValueMember = "Tiempo(s)";
    		chtGrf_TempFilt.Series[2].YValueMembers = "TFilt_sonda3";
    		chtGrf_TempFilt.ChartAreas[0].AxisX.Interval = 150;
    		chtGrf_TempFilt.ChartAreas[0].CursorX.IsUserSelectionEnabled = true;
    		}));	
      }).Start();
    }

    Lo que hace es lo que te comentaba arriba, inicia un thread independiente que obtiene los datos, y cuando los tiene los muestra mediante invocación asíncrona para hacer llamadas seguras.

    Si tienes dudas nos comentas

    Atte


    Jorge Turrado Ferrero

    Mis repositorios en GitHub

    No olvides votar mi comentario si te ha ayudado y marcarlo como respuesta si ha sido la solución, con eso ayudas a mejorar mi reputación en la comunidad y a identificar la respuesta a la gente que tenga el mismo problema.

    Para obtener una respuesta lo más rápida y concisa posible, te recomiendo:

    sábado, 23 de junio de 2018 8:52
  • Gracias Jorge, lo reviso y te digo cosas. Al final he aligerado mucho procesado de los datos rehaciendo la dll y acondicionando allí los datos para no tener que hacerlo en el código de Visual. Pruebo el código que comentas y te digo cosas. 

    Mil gracias. 

    Saludos

    martes, 26 de junio de 2018 15:22
  • Buenas Jorge, he probado el código que me recomendaste y en principio funciona correctamente, la carga de una gran cantidad de datos se hace rápida. Gracias!

    Ahora el principal problema que veo es con el refresco de las gráficas, estas gráficas las tengo en un tab control y cuando voy cambiando de pestaña en ejecución para ir viendo las diferentes curvas, tarda mucho en refrescar los datos, además veo que la memoria del proceso va aumentando escalonadamente conforme voy visualizando gráficas en pestañas hasta el punto de salir un error, el siguiente: 

    System.OutOfMemoryException: 'Se produjo una excepción de tipo 'System.OutOfMemoryException'.'

    No sé cual puede ser el problema. Existe alguna forma de ir liberando esa memoria?

    Gracias de antemano. 

    miércoles, 27 de junio de 2018 8:22
  • Buenas,

    Para poder mirar eso, necesitaríamos ver el resto del código, puede que no este liberando algo correctamente.

    Atte


    Jorge Turrado Ferrero

    Mis repositorios en GitHub

    No olvides votar mi comentario si te ha ayudado y marcarlo como respuesta si ha sido la solución, con eso ayudas a mejorar mi reputación en la comunidad y a identificar la respuesta a la gente que tenga el mismo problema.

    Para obtener una respuesta lo más rápida y concisa posible, te recomiendo:

    miércoles, 27 de junio de 2018 8:28
  • Jorge, el programa completo es demasiado extenso, tengo comunicación con el datalogger para configurarlos además una base de datos donde se registran los diferentes test y condiciones ambientales de los mismos todo ello en diferentes Forms que son llamados desde una form general. Todo el programa corre, normal con una memoria de proceso estable de aprox. 190Mb. Cuando doy a analizar uno de los test, si tiene relativamente pocos datos, la memoria de proceso se incrementa levemente a unos 220Mb aprox. El problema viene con estos test excesivamente largos con mucha cantidad de datos que se ve incrementado a unos 500Mb, y conforme voy abriendo diferentes pestañas para visualizar gráficas, esta memoria va abriéndose progresivamente hasta llegar a 1Gb. Entiendo que el problema reside en esta form de visualización. Espero que con esta información puedas darme alguna pincelada de si se puede resolver el problema y cómo.

    Gracias de nuevo por tu tiempo. 

    miércoles, 27 de junio de 2018 9:09
  • Buenas,

    Tal cual comentas, entiendo que el problema viene de que a medida que abres nuevas pestañas, mantienes en memoria los resultados en las pestañas anteriores, y si por ejemplo un chart de una pestaña necesita 100 Mb para almacenar los datos, cuando abras 5, consumiras 5x100Mb, hasta llegar a un punto donde el sistema no dispone de recursos para mantener todo.

    Es algo así?

    Si ese es el caso, se me ocurren varias maneras de encarar el problema.

    1. Si no aplicas zoom ni ninguna acción a los char más allá de mostrar resultados, podrías reemplazar los charts por imágenes, la idea sería hacer lo mismo que estas haciendo para generar los chart, pero despues hacer una imagen del chart y que sea eso lo que se muestra, así consumirás muchos menos recursos ya que almacenas imágenes en vez de miles de datos.
    2. Si aplicas zoom o utilizas cualquiera de los eventos de un control chart, puedes descargarlos al cambiar de pestaña y volverlos a cargar cuando se vuelve a esa pestaña, el control TabControl tiene un evento que te indica que se ha cambiado de pestaña y a cual se va, podrías usarlo para descargar y cargar charts.
    3. En el caso de que ninguno de los anteriores sirva, otra opción seria limitar el número de pestañas abiertas que se pueden tener.

    Seria posible alguna de esas opciones?

    Quedo a la espera de tu respuesta

    Atte


    Jorge Turrado Ferrero

    Mis repositorios en GitHub

    No olvides votar mi comentario si te ha ayudado y marcarlo como respuesta si ha sido la solución, con eso ayudas a mejorar mi reputación en la comunidad y a identificar la respuesta a la gente que tenga el mismo problema.

    Para obtener una respuesta lo más rápida y concisa posible, te recomiendo:

    miércoles, 27 de junio de 2018 9:39
  • Buenas Jorge, exactamente sucede lo que comentas. De las opciones que comentas descarto la uno ya que efectivamente aplico Zoom además de selección de datos por medio de cursores. La dos más o menos la tengo clara, me faltaría saber cual es ese evento que comentas y por lo que entiendo al lanzarse el evento haría un clear de todas las gráficas que no se encuentran en la pestaña activa. 

    La opción 3, no entiendo a qué te refieres con lo de limitar el número de pestañas abiertas. 

    Quedo a la espera de tus comentarios. 

    Saludos. 

    miércoles, 27 de junio de 2018 9:53
  • Buenas,

    Estas usando un TabControl entiendo, si es asi, tienes una propiedad que se llama  TabCount , con ella puedes saber el numero de pestañas abiertas que tiene el control, cuando quieran abrir una nueva, evaluar si ese valor es menor o igual que "n", y si no cumple, sacar un mensaje diciendo que antes de crear una nueva se debe cerrar alguna, o incluso, cerrarla tu, quitar la que tenga indice 0 (la más antigua), de modo que siempre tengas un número finito de pestañas.

    Respondiendo al tema del evento de la segunda opción, puedes usar varios eventos:  OnDeselected(TabControlEventArgs)/OnDeselecting(TabControlEventArgs) para saber cual se deselecciona y OnSelected(TabControlEventArgs)/OnSelecting(TabControlEventArgs) para saber cual se selecciona, tambien puedes gestionarlo entero tu con SelectedIndezChanged, que te dice el indice da tab que se ha seleccionado.

    Atte


    Jorge Turrado Ferrero

    Mis repositorios en GitHub

    No olvides votar mi comentario si te ha ayudado y marcarlo como respuesta si ha sido la solución, con eso ayudas a mejorar mi reputación en la comunidad y a identificar la respuesta a la gente que tenga el mismo problema.

    Para obtener una respuesta lo más rápida y concisa posible, te recomiendo:

    • Marcado como respuesta GabrieloBurja miércoles, 27 de junio de 2018 13:34
    miércoles, 27 de junio de 2018 10:28
  • Ok Jorge gracias, no te molesto más. Intento esto último del evento ya que los Tab's son fijos y predefinidos por lo que no voy creando nuevos durante la ejecución, por lo que entiendo esta opción de vaciar los charts de los tab que no estoy viendo es la mejor de las opción. 

    Muchísimas gracias. 

    Un saludo. 

    miércoles, 27 de junio de 2018 11:31
  • Okey!!!

    Lo que si te pediría es que cuando pruebes, si alguna respuesta te ha sido útil, la marques como respuesta, con eso ayudas a que otros con tu problema encuentren una solución rápidamente y a subir mi reputación en la comunidad.

    Si no te funciona comenta también y seguimos investigando XD

    Atte


    Jorge Turrado Ferrero

    Mis repositorios en GitHub

    No olvides votar mi comentario si te ha ayudado y marcarlo como respuesta si ha sido la solución, con eso ayudas a mejorar mi reputación en la comunidad y a identificar la respuesta a la gente que tenga el mismo problema.

    Para obtener una respuesta lo más rápida y concisa posible, te recomiendo:

    miércoles, 27 de junio de 2018 12:43