sábado, 9 de enero de 2016

Utilizando NLog con MonoDevelop en .NET

Para cualquier aplicación de software a nivel producción es indispensable tener un componente que escriba los eventos más significativos en una bitácora. Esta acción que se conoce como logging (escribir en la bitácora) se define técnicamente como:

“Una forma sistemática y controlada para obtener el estado de una aplicación en tiempo de ejecución.”

Pensando en esto, los diseñadores de .NET incorporaron un mecanismo de logging de forma predeterminada dentro del ensamblado System.Diagnostics, en clases como Trace, TraceListener, Swicth y todas sus clases derivadas. Aunque el empleo de estas clases es efectivo, no deja de ser rudimentario y carecer de muchas funcionalidades que terminan siendo un límite.

Teniendo en cuenta esto, en el ecosistema .NET han surgido a lo largo de los años una cantidad de componentes propietarios y opensource para el logging de aplicaciones. Dentro de ese conjunto hay un componente que se destacado y es del que trataré de resumir en este tutorial: Nlog.

¿Qué es NLog?

Nlog (http://nlog-project.org/) es un componente open source de logging para .NET, que entre sus características se encuentran:

  • Muy fácil de configurar.
  • Extremadamente personalizable con plantilla (layouts)
  • Altamente extensible

Hay tres características que se deben conocer antes de empezar su utilización:

  1. Targets: Se utilizan para enviar los mensajes hacia otro destino, entiéndase aquí un archivo, una base de datos, un email, la consola, un webservice, etc.
  2. Layouts: Con los comandos de layout podemos definir la estructura o el molde de como acomodar la información escrita en un determinado target.
  3. Levels: Es una forma de asignar una prioridad al mensaje, los niveles permitidos son los siguientes:
    1. Fatal: Sucedió algo que causo que el todo el sistema entero falle, se debe detener la ejecución.
    2. Error: Un problema ha ocurrido pero no es fatal, el sistema puede seguir funcionando.
    3. Warn: Un problema ha ocurrido, pero puede ser recuperable.
    4. Info: Mensajes de información muy útiles como cambios de estado, login/logout, etc.
    5. Debug: Se utiliza durante el desarrollo del sistema.
    6. Trace: Para indicar el inicio y final de una rutina.

Como un primer acercamiento a su utilización, escribí una aplicación de consola en C# que solicita una cadena de conexión, con esta cadena trata de conectase a una base de datos PostgreSQL, si la cadena de conexión no es correcta se utiliza Nlog para enviar la excepción a consola, si logra conectarse solicita una consulta SELECT para ejecutar y mostrar los resultados en la consola. Aquí de nuevo si ocurre una excepción utiliza Nlog para notificarla.

1-. Ejecutar Monodevelop y seleccionar un proyecto de consola y nombrarlo como HelloNlog

Fig. 1 crear un proyecto llamado HelloNlog


2-. Dentro del Solution Explorer has click con el botón derecho y has click en Add Packages , entonces aparecerá la pantalla Add Packages, ya en esa pantalla usa el buscador para encontrar el paquete Nlog, y seleccionar los paquetes: Nlog, Nlog Configuration y Npgsql respectivamente, presionar el botón Add Packages para agregar los ensamblados al proyecto.

Fig. 2 seleccionar los paquetes Nlog y Nlog Configuration


3-. Ahora que ya se tiene una estructura en la solución como se muestra en la siguiente imagen:
Fig. 3 la estructura de la solución con los ensamblados.


4-. Agregar al proyecto una clase llamada NloggerWrapper y escribir el siguiente código:

5-. Bien ahora hay que completar el código de la clase Program con el siguiente código para completar el programa:

6-. Antes de ejecutar la solución es muy importante editar el archivo Nlog.conf para agregar targets (objetivos), layouts (disposición) y rules (reglas). Aquí el código del archivo Nlog.conf del proyecto.

Targets

En este archivo de configuración defino tres targets, el primero hacia un archivo, el segundo hacia una consola con salida de color y el último hacia una consola de salida normal.


Para ver completa la lista de targets consultar el enlace: https://github.com/NLog/NLog/wiki/Targets

Layouts

Por cierta comodidad y porque así es la manera predeterminada de ver la información, puse los layouts de la siguiente manera:


Se especifica un layout por cada uno de los targets Para más información de los layouts ver el siguiente enlace: https://github.com/NLog/NLog/wiki/Layout-Renderers

Rules

Ahora la configuración para las rules, aquí con el '*' le indico que ese nivel se use para todos los logs, únicamente en los niveles Error y Fatal escriban hacia el target llamado fileLog que es el target que escribe hacia un archivo de texto, en la segunda regla indico igual que para todos logs, los niveles Trace se escriban hacia el log llamado consoleLog que tiene la salida normal de consola y por último le indico que para los niveles Warn y Info escriban hacia el log de la consola con colores.


Para más información de las reglas ver el siguiente enlace: https://github.com/nlog/NLog/wiki/Configuration-file#rules

8-. Antes de ejecutar el programa, en el Solution Explorer haz click derecho sobre la solución después haz click en options, aparecerá la ventana Project Options ahí seleccionar las opciones run on external console y pause console output.
Fig. 4 opciones para ejecutar el proyecto.


Bien al ejecutar el programa este solicita una cadena de conexión desde el inicio:
Fig. 5 el programa solicita una cadena de conexión.


Errores como si la cadena de conexión no tiene un formato correcto, el servidor Postgresql esta abajo o no existe la base de datos, etc. Son encerrados dentro por un bloque try/catch y enviados al método LogException para que Nlog utilice el level correspondiente y lo mande hacia el target. Aquí el código del método LogException dentro de la clase NLoggerWrapper

    public static void LogException(Exception ex)
  {
   if (ex is ArgumentException)
    logger.Warn (ex.Message);
   else
   if (ex is NpgsqlException)
    logger.Error (ex.Message);
   else
    logger.Fatal (ex.Message);
  }
En este código dependiendo del tipo de excepción utiliza un nivel (level) de Nlog para escribir.
Fig. 6 el programa con Nlog muestra las excepciones en la consola con color..


Fig. 7 excepción atrapada por Nlog que se muestra en color.


Cuando uno de los target como en este ejemplo esta dirigido a escribir en archivo se puede revisar la creación del archivo y posteriormente su contenido.

Fig. 8 la creación de los archivos log con el target File.


Fig. 9 el formato de los archivos log.


Fig. 10 la ejecucción del programa sin errores


Download el código fuente para Xamarin Studio o Visual Studio

lunes, 4 de enero de 2016

Utilizando la clase MemoryStream con imágenes en GTK# y MonoDevelop

El mecanismo de comunicación entre las aplicaciones .NET y los dispositivos de entrada y de salida se realiza mediante streams (flujos secuenciales) de bytes que se obtienen de una fuente de entrada hacia la aplicación y salen de la aplicación hacia una fuente de salida, pensemos en los flujos como corrientes de agua que van de un recipiente a otro.

Dentro del ensamblado System.IO existe la clase abstracta Stream que implementa operaciones de lectura y escritura sincrónica y asíncrona y que sirve como base para cualquier clase derivada que sea utilizada en operaciones de entrada y salida, como ejemplos de clases derivadas tenemos a FileStream, CriptoStream, NetworkStream, BufferedStream y MemoryStream. De estas clases la clase MemoryStream proporciona una área de almacenamiento temporal en memoria o sea un arreglo de bytes sin signo, entre sus usos más comunes se encuentran las operaciones con imágenes, la compresión y descompresión de archivos, el cifrado/descifrado de datos y la serialización entre otros.

Algunas propiedades y métodos de la clase MemoryStream:

    Read: Inicia la operación de lectura de forma secuencial de un número de bytes desde el inicio hasta el final del flujo.
  • ReadByte: Idéntico que Read solo que lee un byte de forma secuencial.
  • Length: Es la longitud en bytes del flujo.
  • Position: Obtiene la posición del apuntador lector del flujo.
  • ReadTimeOut: Se refiere al tiempo de espera para operaciones de lectura.
  • WriteTimeOut: Igual que ReadTimeOut solo que para operaciones de escritura.
  • Flush: Vacia el contenido de los buffers del flujo y obliga a que se escriban en el flujo.
  • GetBuffer: Regresa un arreglo de bytes sin signo que fueron utilizados para crear el flujo.
  • Seek: Coloca el apuntador lector del flujo en una determinada posición.

La clase MemoryStream se considera una buena solución siempre que se trate con grandes cantidades de datos y se requiera una rápida lectura y escritura de los mismos. Un detalle importante es que si en el constructor de la clase se establece el tamaño del arreglo, después no es posible cambiar el tamaño. Por eso es mejor utilizar un constructor vacío e ir utilizando el arreglo según se requiera.

Como ejemplo de su uso, utilizamos una sencilla proyecto GTK# de monodevelop que lee una imagen del disco duro, crea una copia de la imagen en un flujo memorystream para que esa imagen pueda ser ajustada a las medidas que el usuario proporcione y después pueda guardarla con el formato que elija de la aplicación. El diseño del GUI (Graphical User Interface) de la aplicación se muestra en la siguiente imagen, utilizando el designer de MonoDevelop.

Para este proyecto es necesario hacer referencias a los ensamblados: System.Drawing, System.Drawing.Image, System.IO como se muestra en la siguiente imagen:

El listado completo de la clase principal es el siguiente:

 using System;
using Gtk;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;


public partial class MainWindow: Gtk.Window
{ 
 MemoryStream ms = null;
 public MainWindow (): base (Gtk.WindowType.Toplevel)
 {
  Build ();
 }
 
 protected void OnDeleteEvent (object sender, DeleteEventArgs a)
 {
  Application.Quit ();
  a.RetVal = true;
 }
 
 
 protected void OnFlBtnFileSelectionChanged (object sender, EventArgs e)
 {
  try
  {
  if(string.IsNullOrEmpty(flBtnFile.Filename))
   ShowMessageBox("Please select a file.");
  else
  {
   byte[] fileBytes = GetBytesFromFile(flBtnFile.Filename);
   ms = new MemoryStream(fileBytes);
   image2.Pixbuf = new Gdk.Pixbuf(ms.ToArray());
  }
  }catch(Exception ex){
   Console.WriteLine(ex.Message);
  }
 }
 
 protected virtual void OnBtnSaveAsClicked (object sender, System.EventArgs e)
 {
  string filename = null;
  ImageFormat imageFormat = (rbjpg.Active ? ImageFormat.Jpeg : (rbwmf.Active ? ImageFormat.Wmf:
                (rbpng.Active ? ImageFormat.Png : ImageFormat.Bmp)));
  try{
   Gtk.FileChooserDialog fc=  new Gtk.FileChooserDialog("Save image",
                              this,
                              FileChooserAction.Save,
                              "Cancel",ResponseType.Cancel,
                              "Save",ResponseType.Accept);
   if (fc.Run() == (int)ResponseType.Accept) {
    filename = fc.Filename + "." + imageFormat.ToString();
    fc.Hide();
    fc.Dispose();
    using(FileStream foutput = 
new FileStream(filename,FileMode.Create,FileAccess.Write))
    {
     Bitmap bmp = new Bitmap(ms);
     bmp.Save(foutput,imageFormat);
    }
   }
   else{
    fc.Hide();
    fc.Dispose();
   }
   
  }catch(Exception ex){
   ShowMessageBox(ex.Message);
  }
 } 
 
 
 protected virtual void OnBtnMakeThumbClicked (object sender, System.EventArgs e)
 {
  int w,h;
  try{
   if(!string.IsNullOrEmpty(txtWidth.Text) && 
      !string.IsNullOrEmpty(txtHeight.Text))
   {
    w = Convert.ToInt32(txtWidth.Text);
    h = Convert.ToInt32(txtHeight.Text);
    Bitmap bitmap = new Bitmap(ms);
    System.Drawing.Image.GetThumbnailImageAbort thumbnailCallback = 
    new System.Drawing.Image.GetThumbnailImageAbort(delegate(){ return false; });
    System.Drawing.Image thumbnail = 
bitmap.GetThumbnailImage(w,h,thumbnailCallback,IntPtr.Zero);
    byte[] thumbnailBytes = 
((byte[])new ImageConverter().ConvertTo(thumbnail,typeof(byte[])));
    ms = new MemoryStream(thumbnailBytes);
    image2.Pixbuf = new Gdk.Pixbuf(ms.ToArray());
   }
   else
    ShowMessageBox("width & height must be integers.");
  }catch(ApplicationException ex){
   ShowMessageBox(ex.Message);
  }
 }
 
 byte[] GetBytesFromFile(string filename){
  byte[] imgBytes;
  using(FileStream fis = new FileStream(filename,FileMode.Open,FileAccess.Read))
  {
   BinaryReader reader = new BinaryReader(fis);
   imgBytes = reader.ReadBytes((int)fis.Length);
   reader.Close();
  }
  return imgBytes;
 }
 
 
void ShowMessageBox(string msg){
using (Dialog dialog = new MessageDialog (this,
      DialogFlags.Modal | DialogFlags.DestroyWithParent,
      MessageType.Info,
      ButtonsType.Ok,
      msg)) {
dialog.Run ();
dialog.Hide ();
}
}
 
} 

Al oprimir F5 (Debug) ó Ctrl + F5 en Monodevelop se ejecutará la aplicación como se ve en la siguiente imagen:

Cuando se presiona el botón de “Select image” se ve el FileDialog para escoger la imagen desde el disco duro:

Una vez seleccionada la imagen se mostrará dentro del programa, aunque esa imagen no son los bytes originales del archivo sino los bytes que están contenidos en el buffer del objeto MemoryStream.

Una vez cargada la imagen puedes redimensionarla (en pixeles) de acuerdo a la anchura y la altura que teclees en los valores de los campos de texto width y height. Al presionar el botón “Change size”la imagen se ajustará a las nuevas dimensiones.

Una segunda imagen como ejemplo con diferentes medidas:

Puedes guardar la imagen modificada con utilizando uno de los formatos que están en los radiobuttons

Al presionar el botón “Save As” se abrirá un cuadro de dialogo (filechooser) para guardar la imagen en cualquier parte del sistema de archivos.

Podemos ver la imagen guardada en el sistema de archivos.

La aplicación inicia cuando se escoge un archivo de imagen del sistema de archivos para obtener su matriz de bytes. Esto lo hacemos con el siguiente código:

 byte[] GetBytesFromFile(string filename){
 byte[] imgBytes;
 using(FileStream fis = 
new FileStream(filename,FileMode.Open,FileAccess.Read))
 {
  BinaryReader reader = new BinaryReader(fis);
  imgBytes = reader.ReadBytes((int)fis.Length);
  reader.Close();
 }
  return imgBytes;
 } 

Entonces podemos esa matriz de bytes en el flujo MemoryStream y colocamos su buffer en el control (widget) Image.

 ms = new MemoryStream(fileBytes);
image2.Pixbuf = new Gdk.Pixbuf(ms.ToArray()); 

Aquí ya la aplicación esta lista para redimensionar la imagen esto se logra con el código del evento para el botón “Change Size”.

 protected virtual void OnBtnMakeThumbClicked (object sender, System.EventArgs e)
 {
  int w,h;
  try{
   if(!string.IsNullOrEmpty(txtWidth.Text) && 
      !string.IsNullOrEmpty(txtHeight.Text))
   {
    w = Convert.ToInt32(txtWidth.Text);
    h = Convert.ToInt32(txtHeight.Text);
    Bitmap bitmap = new Bitmap(ms);
    System.Drawing.Image.GetThumbnailImageAbort thumbnailCallback = 
    new System.Drawing.Image.GetThumbnailImageAbort(delegate(){ return false; });
    System.Drawing.Image thumbnail = bitmap.GetThumbnailImage(w,h,thumbnailCallback,IntPtr.Zero);
    byte[] thumbnailBytes = 
((byte[])new ImageConverter().ConvertTo(thumbnail,typeof(byte[])));
    ms = new MemoryStream(thumbnailBytes);
    image2.Pixbuf = new Gdk.Pixbuf(ms.ToArray());
   }
   else
    ShowMessageBox("width & height must be integers.");
  }catch(ApplicationException ex){
   ShowMessageBox(ex.Message);
  }
 } 

En este código se utiliza el método GetThumbnailImage de la clase Image:

 System.Drawing.Image thumbnail = bitmap.GetThumbnailImage(w,h,thumbnailCallback,IntPtr.Zero); 

NOTA: para este código hay que poner toda la ruta de los ensamblados para la clase image ya que exiten dos clases Image una en el ensamblado Gtk y otra en el ensambaldo System.Drawing por lo que hay que diferenciarlas o el compilador se confundirá.

De nueva cuenta se convierte la imagen en una matriz de bytes y la ponemos dentro del buffer del objeto MemoryStream, para después ponerla en el widget de imagen, esto lo realiza las siguientes líneas de código:

 
byte[] thumbnailBytes = 
((byte[])new ImageConverter().ConvertTo(thumbnail,typeof(byte[])));
 ms = new MemoryStream(thumbnailBytes);
image2.Pixbuf = new Gdk.Pixbuf(ms.ToArray()); 

Por último el código del botón para guardar la imagen en el sistema de archivos con el formato seleccionado en los botones de radio.

 protected virtual void OnBtnSaveAsClicked (object sender, System.EventArgs e)
 {
  string filename = null;
  ImageFormat imageFormat = (rbjpg.Active ? ImageFormat.Jpeg : (rbwmf.Active ? ImageFormat.Wmf:
                (rbpng.Active ? ImageFormat.Png : ImageFormat.Bmp)));
  try{
   Gtk.FileChooserDialog fc=  new Gtk.FileChooserDialog("Save image",
                              this,
                              FileChooserAction.Save,
                              "Cancel",ResponseType.Cancel,
                              "Save",ResponseType.Accept);
   if (fc.Run() == (int)ResponseType.Accept) 
{
    filename = fc.Filename + "." + imageFormat.ToString();
    fc.Hide();
    fc.Dispose();
    using(FileStream foutput = 
new FileStream(filename,FileMode.Create,FileAccess.Write))
   {
     Bitmap bmp = new Bitmap(ms);
     bmp.Save(foutput,imageFormat);
    }
   }
   else{
    fc.Hide();
    fc.Dispose();
   }
   
  }catch(Exception ex){
   ShowMessageBox(ex.Message);
  }
 } 

Aquí se utiliza la clase FileStream la cual representa un flujo hacia un archivo para guardar la imagen que se construye a partir de la matriz de imagen del buffer del objeto MemoryStream.

using(FileStream foutput = 
new FileStream(filename,FileMode.Create,FileAccess.Write))
{
 Bitmap bmp = new Bitmap(ms);
 bmp.Save(foutput,imageFormat);
}

Descarga el código fuente en un proyecto para MonoDevelop o Visual Studio

Obteniendo las características del browser en ASP.NET

La portabilidad en el browser (navegador) ha sido siempre un tema discutible desde los primeros inicios del desarrollo Web.

Aunque durante todos estos años han habido avances en el uso de estándares para el desarrollo Web y los niveles de incompatibilidad en los navegadores se han disminuido, no se puede garantizar que una página o interfaz web conserve su aspecto (look-and-feel) o sus funcionalidades en todos los dispositivos y navegadores cliente que la muestren.

Es por eso que las aplicaciones ASP.NET con frecuencia necesitan conocer las características del browser (navegador) para ejecutar un código personalizado que se asegure que una página funcionará correctamente independientemente del navegador donde se despliegue.

Para consultar los tipos de browser y sus características ASP .NET consulta la clase HttpBrowserCapabilities, este objeto encapsula la información que envía el cliente durante una petición HTTP.

Con el siguiente código se ejemplifica la utilización de esta clase en una página ASP .NET que despliega la información del browser.

Fig 1. El código markup del programa.


Toda la funcionalidad se encuentra al consultar la clase Request.Browser que regresa una clase System.Web.HttpBrowserCapabilities.

Fig 2. El código de la clase C# (CodeBehind).


Al ejecutar la página ASP.NET, el resultado es el siguiente:


Download el código fuente.