lunes 20 de junio de 2011

[Winforms] Control global de Errores – Implementar Log

 

Introducción

He notado en varias oportunidades que en ciertas circunstancias surgen errores no controlador que son difíciles de rastrear

Estos es consecuencia de un descuido, al no agregar correctamente en todos los puntos importantes el try..catch, o simplemente por la lógica de la aplicación por se grande implicaría un gran esfuerzo poner en cada evento el control de errores.

Es por eso que definir un control global a nivel de aplicación podría ayudar, atrapando el problema y registrando que sucedió y donde, esta información es muy preciada cuando se esta perdido y no se sabe que produce el problema, mas cuando se esta en el entorno de producción y no se cuenta con una herramienta de debug

En este artículo se trataran los siguientes temas:

  1. Control global de errores
  2. Log usando System.IO.Log

 

1- Control Global Errores

El implementar la lógica para controlar globalmente los errores no es nada difícil, el truco esta en adjuntarse a un evento, concretamente:

Application.ThreadException

Un buen lugar para realizar esta asignación del evento es el archivos Program.cs, dentro del método Main()

[C#]

[STAThread]
static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    Application.ThreadException += new ThreadExceptionEventHandler(Application_ThreadException);

    Application.Run(new Form1());
}

static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
{
    MessageBox.Show(e.Exception.Message);
}

Para lograr esto mismo en vb.net requiere de algunos paso adicionales, ya que vb.net no brinda un acceso transparente al método Main, ni permite que este sea asignado como inicio de la aplciacion, pero en este articulo

Winforms, realizar tareas antes de inicializar aplicación

explico como destrabar esta situación para poder así implementarlo como lo harían en c#, definiendo el Main() como inicio de la aplicación.

[VB.NET]

<STAThread()> _
Friend Shared Sub Main()

    Application.EnableVisualStyles()
    Application.SetCompatibleTextRenderingDefault(False)

    AddHandler Application.ThreadException, AddressOf Application_ThreadException

    Application.Run(New Form1())

End Sub

Private Shared Sub Application_ThreadException(ByVal sender As Object, ByVal e As System.Threading.ThreadExceptionEventArgs)

    MessageBox.Show(e.Exception.Message)

End Sub

El código en ambos lenguajes preserva el mismo concepto, pero la forma en como se adjunta el evento difiera bastante.

Entonces, ahora si sucediera algo como lo reflejado en la imagen

O sea, si se ingresara caracteres en el calculo, el no atrapar el error en un bloque try..catch, haría que la aplicación finalice de forma brusca cerrándose, pero al tener definido el evento ThreadException, entraría en acción tomando el error y mostrando el mensaje.

Con estos simples paso ya tenemos el control global de errores implementado.

 

2- Log usando System.IO.Log

Si bien en una primer instancia hacer uso de un mensaje podría ayudar a detectar el problema en la aplicación, este podría evolucionar en un log a un archivo para dejar tracking de lo sucedido, en este caso no solo se pondría el mensaje del problema, sino que además se podría agregar el StackTrace para poder analizar que métodos se fueron ejecutando hasta causar el fallo.

En este caso el log a un archivos será muy simple

[C#]

static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
        {
            using(FileRecordSequence record = new FileRecordSequence("application.log", FileAccess.Write))
            {

                string message = string.Format("[{0}]Message::{1} StackTrace:: {2}", DateTime.Now, 
                                                                                    e.Exception.Message, 
                                                                                    e.Exception.StackTrace);

                record.Append(CreateData(message), SequenceNumber.Invalid, 
                                                            SequenceNumber.Invalid, 
                                                            RecordAppendOptions.ForceFlush);
            }
        }


        private static IList<ArraySegment<byte>> CreateData(string str)
        {
            Encoding enc = Encoding.Unicode;

            byte[] array = enc.GetBytes(str);

            ArraySegment<byte>[] segments = new ArraySegment<byte>[1];
            segments[0] = new ArraySegment<byte>(array);

            return Array.AsReadOnly<ArraySegment<byte>>(segments);
        }

[VB.NET]

Private Shared Sub Application_ThreadException(ByVal sender As Object, ByVal e As System.Threading.ThreadExceptionEventArgs)

    Using record As New FileRecordSequence("application.log", FileAccess.Write)

        Dim message As String = String.Format("[{0}]Message::{1} StackTrace:: {2}", DateTime.Now, e.Exception.Message, e.Exception.StackTrace)

        record.Append(CreateData(message), SequenceNumber.Invalid, SequenceNumber.Invalid, RecordAppendOptions.ForceFlush)
    End Using

End Sub


Private Shared Function CreateData(ByVal str As String) As IList(Of ArraySegment(Of Byte))

    Dim enc As Encoding = Encoding.Unicode

    Dim _array As Byte() = enc.GetBytes(str)

    Dim segments As ArraySegment(Of Byte)() = New ArraySegment(Of Byte)(0) {}
    segments(0) = New ArraySegment(Of Byte)(_array)

    Return Array.AsReadOnly(Of ArraySegment(Of Byte))(segments)

End Function

Ahora el evento global de errores tiene bastante mas código, en donde se arma un mensaje bastante mas útil, el cual será enviado a la funcionalidad de log para registrar el suceso.

Para recuperar el archivo, solo deben ir a la ubicación donde esta el .exe, en este caso debería esta en la carpeta \bin\Debug del proyecto.

Lo mas probable es que el archivo no se legible a simple vista porque este sistema de log trabaja con el concepto de entradas de registros, es por eso que se confecciono un formulario muy simple para poder recuperar la información de forma visual.

[C#]
[VB.NET]

9 comentarios:

LeoB dijo...

Muy buen articulo... Gracias por compartir

hgjhgj dijo...

Hola, disculpa pero no logro entender como se utiliza tu aplicacion, ejecuto la demo del calculo, ingreso un numero y una letra y el error me dice: "La cadena de entrada no tiene el formato correcto.", luego intento abrir la ventana de "LogViewer", para supuestamente leer el error, pero no veo nada!, que estoy haciendo mal?

gracias!!

Leandro Tuttini dijo...

hola hgjhgj

que raro, deberias ver el log del error

si editas con el notepad el archivo donde se registro el error que generaste, puedes ver que hay texto alli, aunque no se entieneda del todo el contenido del archivo, pero hay algo que indique un registro del problema

has validado que tengas permiso de escritura al archivo del log

saludos

Carlos dijo...

La explicación del Application.ThreadException es exactamente lo que estaba buscando, mil gracias!!!

K-loca dijo...

No tiene nada que ver con el articulo pero tengo una duda:

tengo que generar un reporte similar a esto:
campo 1 campo2
11 4 5
12 1 2

donde campo uno y campo 2 son enfermedades y 11 y 12 son tipos de dientes como puedo contar cuantos dientes de tipo 11 tienen la enfermedad tipo 1 y cuantos dientes tipo 11 tienen la enfermedad tipo dos sin tener que hacer una consulta como esta:
select count(campo1) from tabla campo=12
para saber cuantos hay celda por celda

K-loca dijo...

con respecto a la tabla mas claro seria
campo1 campo2
11 1 2
12 3 1

Leandro Tuttini dijo...

hola K-loca

el tema es que sino entendi mal, las enfermedades del duente deberian ser registro y no campo

porque usas campos para la enfermedad
no seri mas simple si la tabla es del tipo

TipoDienteEnfermedades (tabla)
idtipoenfermedad PK
TipoDiente
idEnfermedad FK

Enfermedades(tabla)
idenfermedad PK
descripcion


entonces los datos podrian ser

idtipoenfermedad | diente | idEnfermedad
1 | 11 | 1
2 | 11 | 2
3 | 12 | 2


Nota: no respeta muy bien el diseñod e la tabla por eso use "|" para separar cada campo

como veras ahora es mas simple de contar porque als enfermedades son un campo relacionado con la tabla de enfermedades

ahora si puedes agrupar

SELECT E.descripcion, COUNT(*) FROM DienteEnfermedades DE INNER JOIN Enfermedades E
ON DE.idenfermedad = E.idenfermendad
WHERE tipodiente = 11


saludos

K-loca dijo...

gracias

K-loca dijo...

sobre mi duda a ver si me pude explicar mejor aqui esta:

http://jjcaloca.blogspot.com/