ASP.Net Enviar correo electrónico con ficheros adjuntos

Otra de las preguntas bastante frecuente por los foros de Microsoft es la posibilidad de enviar un correo electrónico desde una página pero que contenga uno o varios ficheros adjuntos. Aunque en una búsqueda genérica nos aparecen miles de entradas sobre el tema, intentaremos, en esta ocasión, crear un ejemplo lo más sencillo posible.

El envío de un correo electrónico es relativamente sencillo, sobre todo, si nos abstraemos de la configuración del correo y dejamos los datos en el archivo web.config.

   1: <system.net>
   2:   <mailSettings>
   3:     <smtp from="cuentaValidaDelServidor">
   4:       <network host="servidorSMTP"
   5:                userName ="usuarioServidorSMTP"
   6:                password="contraseñaServidorSMTP"
   7:                />
   8:     </smtp>
   9:   </mailSettings>
  10: </system.net>

De esta manera, el envío de correo sería algo así como:

   1: void enviaCorreo()
   2: {
   3:     using (System.Net.Mail.MailMessage message = new System.Net.Mail.MailMessage())
   4:     {
   5:         // Dirección de destino
   6:         message.To.Add("cuentaDestino");
   7:         // Asunto 
   8:         message.Subject = "Asunto del mensaje";
   9:         // Mensaje 
  10:         message.Body = "Cuerpo del mensaje";
  11:  
  12:         // Se envía el mensaje
  13:         System.Net.Mail.SmtpClient smpt = new System.Net.Mail.SmtpClient();
  14:         smpt.Send(message);
  15:     }
  16: }

Ahora que ya tenemos definido el envío del correo, nos podemos centrar en cómo hacerlo desde una página ASP.Net.

Para el envío de un mensaje estándar (sin datos adjuntos) bastaría con definir unos cuadros de texto para el destinatario, el asunto y el mensaje, pero a la hora de enviar ficheros adjuntos podemos encontrarnos con que otro problema, pues deberemos tener en cuenta lo siguiente:

  • Si vamos a enviar ficheros adjuntos, éstos deberán encontrarse en el servidor, nunca el cliente
  • Salvo que usemos controles externos, el control UploadFile sólo nos permite subir un fichero a la vez
  • queremos que el número de ficheros a enviar sea variable.

Con estas premisas, sólo nos queda hacer una cosa:

  1. Deberemos subir los ficheros, de uno en uno y previo al envío del correo.
  2. El usuario se puede equivocar a la hora de subir un fichero y debería poder borrarlo

Esto nos lleva a pensar que, además de tener los ficheros en una carpeta del servidor, deberemos añadir los nombres de dichos ficheros en algún control, de tal manera que el usuario pueda modificarlo. Para el ejemplo actual escojo uno sencillo, el ListBox, que nos permitirá mostrar al cliente los ficheros subidos y, en caso necesario, podrá eliminar alguna entrada. A nivel de diseño, creo una tabla con los controles UploadFile y el ListBox que encapsulo dentro de un UpdatePanel.

   1: <asp:UpdatePanel ID="pnUpdateFile" runat="server" UpdateMode="Conditional">
   2:     <Triggers>
   3:         <asp:PostBackTrigger ControlID="cmdAddFile" />
   4:     </Triggers>
   5:     <ContentTemplate>
   6:         <table>
   7:             <tr>
   8:                 <td style="width: 450px;">
   9:                     <asp:FileUpload ID="fUpload" runat="server" /></td>
  10:                 <td style="width: 50px;">
  11:                     <asp:Button ID="cmdAddFile" 
  12:                         runat="server" 
  13:                         Text="+" 
  14:                         ToolTip="Añade el fichero a la lista"
  15:                         OnClick="cmdAddFile_Click" />
  16:                 </td>
  17:             </tr>
  18:             <tr>
  19:                 <td>
  20:                     <asp:ListBox ID="lstFiles" runat="server"></asp:ListBox>
  21:                 </td>
  22:                 <td>
  23:                     <asp:Button ID="cmdDelFile" 
  24:                         runat="server" 
  25:                         Text="-" 
  26:                         ToolTip="Elimina el fichero seleccionado de la lista"
  27:                         OnClick="cmdDelFile_Click" />
  28:                 </td>
  29:             </tr>
  30:         </table>
  31:     </ContentTemplate>
  32: </asp:UpdatePanel>

Bueno, ahora que ya tenemos definida la estructura HTML procederemos a implementar el code-behind delos botones que nos permitirán subir ficheros al servidor (+) y eliminarlos (-)

   1: private string tempPath = @"~/uploads/temp";
   2:  
   3:    protected void cmdAddFile_Click(object sender, EventArgs e)
   4:    {
   5:  
   6:        FileUpload f = fUpload;
   7:  
   8:        // No se hace nada si no hay fichero
   9:        if (!f.HasFile)
  10:            return;
  11:  
  12:        // Se crea un Item para el ListBox
  13:        //  - Value: Nombre del fichero
  14:        //  - Text : Texto para mostrar
  15:        ListItem item = new ListItem();
  16:        item.Value = f.FileName;
  17:        item.Text = f.FileName + 
  18:                    " (" + f.FileContent.Length.ToString("N0") + 
  19:                    " bytes).";
  20:  
  21:        // Se sube el fichero a la carpeta temporal
  22:        f.SaveAs(Server.MapPath(Path.Combine(tempPath, item.Value)));
  23:  
  24:        // Se deja el nombre del fichero en el ListBox
  25:        lstFiles.Items.Add(item);
  26:    }
  27:  
  28:    protected void cmdDelFile_Click(object sender, EventArgs e)
  29:    {
  30:        ListBox lb = lstFiles;
  31:        // Se comprueba que exista algún item seleccionado
  32:        if (lb.SelectedItem == null)
  33:            return;
  34:  
  35:        // Se elimina el fichero seleccionado
  36:        borraEntrada(lb.SelectedItem.Value);
  37:    }
  38:  
  39:    /// <summary>
  40:    /// Elimina el fichero de la carpeta temporal y del ListBox.
  41:    /// </summary>
  42:    /// <param name="fileName"></param>
  43:    private void borraEntrada(string fileName)
  44:    {
  45:        string fichero = Server.MapPath(Path.Combine(tempPath, fileName));
  46:        File.Delete(fichero);
  47:  
  48:        ListItem l = lstFiles.Items.FindByValue(fileName);
  49:        if (l != null)
  50:            lstFiles.Items.Remove(l);
  51:    }

 

Una vez hemos cumplido los requisitos iniciales, ya tan sólo nos queda crear el botón para que envíe el mensaje, leyendo los archivos de los Items del ListBox.

   1: /// <summary>
   2:   /// Envía el correo electrónico.
   3:   /// Los datos de configuración del servidor de correo SMTP se configuran 
   4:   /// en el fichero web.config.
   5:   /// </summary>
   6:   private void enviaCorreo()
   7:   {
   8:       using (System.Net.Mail.MailMessage message = new System.Net.Mail.MailMessage())
   9:       {
  10:           // Dirección de destino
  11:           message.To.Add(txtPara.Text);
  12:           // Asunto 
  13:           message.Subject = txtAsunto.Text;
  14:           // Mensaje 
  15:           message.Body = txtMensaje.Text;
  16:  
  17:           // Se recuperan los ficheros
  18:           foreach (ListItem l in lstFiles.Items)
  19:           {
  20:               // Lectura del nombre del fichero
  21:               string fichero = Server.MapPath(Path.Combine(tempPath, l.Value));
  22:  
  23:               // Adjuntado del fichero a la colección Attachments
  24:               message.Attachments.Add(new System.Net.Mail.Attachment(fichero));
  25:           }
  26:  
  27:           // Se envía el mensaje y se informa al usuario
  28:           System.Net.Mail.SmtpClient smpt = new System.Net.Mail.SmtpClient();
  29:           string mensaje = string.Empty;
  30:           try
  31:           {
  32:               smpt.Send(message);
  33:               mensaje = "Correo enviado con éxito";
  34:           }
  35:           catch (Exception ex)
  36:           {
  37:               mensaje = "Ocurrió un error: " + ex.Message;
  38:           }
  39:           resultado.Text = mensaje;
  40:       }
  41:  
  42:       // Se borran los ficheros de la carpeta temporal
  43:       while (lstFiles.Items.Count > 0)
  44:       {
  45:           borraEntrada(lstFiles.Items[0].Value);
  46:       }
  47:   }

Como va siendo habitual, por si me he dejado algo en el Copy & Paste o por si alguien quiere descargar el ejemplo, dejo un enlace con la aplicación. Como se puede apreciar, he intentado hacer uso de Themes y hoja de estilos externas para dejar en la página de envío tan sólo aquellos elementos que se refieran al envío del correo.

Enlace

C# Encuadrar una imagen mediante GDI+

En los foros de MSDN es frecuente leer alguna pregunta de cómo se puede encuadrar una fotografía para poder adaptarla a un determinado tamaño. A menudo las respuestas se enfocan al entorno de desarrollo, así por ejemplo, en el caso de ser en el foro de ASP.Net, la respuesta comúnmente se enfoca a la creación de un DIV que permita simular el cuadro externo, en caso de WinForms, un panel que recubra la imagen… No obstante, cuando tratamos de hacer algo así, casi siempre nos encontramos con problemas adicionales puesto que no todas las imágenes que almacenamos tienen ni la misma resolución ni la misma orientación.

Pongamos un ejemplo que seguro que entenderemos mejor los problemas. Para el ejemplo opto por usar WebForms y busco por internet tres fotografías diferentes de la Sagrada Familia de Barcelona con resoluciones y orientación diferentes: 160×230, 640×480 y 4000×3000.

Crear el entorno ASP.Net

Para simular el marco de la fotografía, lo que hago es crear un div que envolverá la fotografía

   1: <div id="showFoto">
   2:     <asp:Image ID="foto" runat="server"   />
   3: </div>

Y, lógicamente, defino los estilos:

   1: #showFoto
   2: {
   3:     background-color: #FFFF00;
   4:     border: thin inset #FFFF00;
   5:     width: 500px;
   6:     height:375px;
   7:     margin: 0 auto;
   8: }
   9:  
  10: #showFoto img
  11: {
  12:     position:relative;
  13:     top: 50%;
  14:     left: 50%;
  15:     width: 460px;
  16:     height:345px;
  17:     margin-top: -172px;
  18:     margin-left: -230px;
  19: }

Con esto un resultado más o menos óptimo en IE9, Firefox 9 y Chrome 16 siempre y cuando la fotografía tenga una orientación horizontal.

image

Eso sí, al haber hecho uso de medidas fijas tanto para el ancho como para el alto de la capa y de la foto, en el caso de que ésta última tenga una orientación vertical, podemos decir que no queda tan bonito puesto que se deforma la imagen.

image

Démosle la vuelta a la tortilla

No sé si existe algún elemento desde el HTML o los CSS que solventen este problema pero aun así, siempre nos encontraríamos con el mismo problema si acudimos a WinForms, WPF… Le daré un vuelco al planteamiento y lo que intentaré será, desde C#, añadir un marco de un tamaño variable a la imagen y crear los métodos necesarios para que se adapte una imagen redimensionada al marco. Por tanto, creo una clase con una serie de métodos públicos que permitan realizar tal acción.

image

Dos propiedades RecuadroExterior y CapasDegradado permitirán definir el tamaño de la nueva imagen y el ancho del marco respectivamente; y tres métodos sobrecargados que intentarán facilitarnos el trabajo según el origen sea un array de bytes extraído de la base de datos, una imagen o, sencillamente, la ruta completa de un fichero.

Para la conversión del array de bytes en Imagen (o si necesitamos la inversa) uso una rutina muy conocida por los foros de ASP.Net que nos lo permite hacer:

   1: /// <summary>
   2:  /// Convierte un array de bytes en una imagen
   3:  /// </summary>
   4:  /// <remarks>
   5:  /// Extraido de http://www.daniweb.com/software-development/csharp/code/365920
   6:  /// </remarks>
   7:  public static Image byteArrayToImage(byte[] byteArrayIn)
   8:  {
   9:      MemoryStream ms = new MemoryStream(byteArrayIn);
  10:      Image returnImage = Image.FromStream(ms);
  11:      return returnImage;
  12:  }
  13:  
  14:  /// <summary>
  15:  /// Convierte una imagen en un array de bytes
  16:  /// </summary>
  17:  /// /// <remarks>
  18:  /// Extraido de http://www.daniweb.com/software-development/csharp/code/365920
  19:  /// </remarks>
  20:  public static byte[] imageToByteArray(System.Drawing.Image imageIn)
  21:  {
  22:      MemoryStream ms = new MemoryStream();
  23:      imageIn.Save(ms, System.Drawing.Imaging.ImageFormat.Gif);
  24:      return ms.ToArray();
  25:  }

Para las propiedades de los tamaños abogo por establecer unos valores más o menos estándares sin olvidar que podemos modificarlos en cada momento:

   1: /// <summary>
   2: /// Obtiene o establece el tamaño del cuadro de salida
   3: /// Por defecto: 640 x 480
   4: /// </summary>
   5: public Size RecuadroExterior
   6: {
   7:     get { return _recuadroExterior; }
   8:     set { _recuadroExterior = value; }
   9: }
  10:  
  11: /// <summary>
  12: /// Obtiene o establece las capas del marco, es decir, su ancho en 
  13: /// píxeles
  14: /// </summary>
  15: public int CapasDegradado
  16: {
  17:     get { return _capasDegradado; }
  18:     set { _capasDegradado = value; }
  19: }
  20:  
  21: private Size _recuadroExterior = new Size(640, 480);
  22: private int _capasDegradado = 20;

Una vez creado más o menos el entorno, nos falta definir la metodología a seguir que, más o menos, será la siguiente:

  1. Lectura de la imagen inicial
  2. Creación de una nueva imagen con el tamaño definido en RecuadroExterior
  3. Creación del marco
  4. Comparar el tamaño de la imagen inicial con el tamaño del marco.
    1. Si la imagen es mayor que el marco, se redimensionará adaptándola al extremo más amplio
  5. Se centrará la imagen tanto horizontal como verticalmente.

Los métodos de trabajo.

Pasaremos ahora a la implementación de los métodos y comenzaremos por los métodos sobrecargados que devolverán la imagen:

   1: /// <summary>
   2:  /// Obtiene una imagen en base al parámetro <para>fichero</para>
   3:  /// </summary>
   4:  /// <param name="fichero">
   5:  /// Ruta completa del fichero gráfico que se quiere encuadrar
   6:  /// </param>
   7:  /// <returns>
   8:  /// Imagen encuadrada y sin distorsionar con el mayor tamaño posible
   9:  /// </returns>
  10:  public Image GetImagenConMarco(string fichero)
  11:  {
  12:      Image img = Image.FromFile(fichero);
  13:      return GetImagenConMarco(img);
  14:  }
  15:  
  16:  
  17:  /// <summary>
  18:  /// Obtiene una imagen encuadrada con el array de bytes pasado
  19:  /// como parámetro
  20:  /// </summary>
  21:  /// <param name="bytes">
  22:  /// Array de bytes que representa una imagen
  23:  /// </param>
  24:  /// <returns>
  25:  /// Imagen encuadrada y sin distorsionar con el mayor tamaño posible
  26:  /// </returns>
  27:  /// <remarks>Se crea este método para facilitar el trabajo
  28:  /// al extraer imágenes de una base de datos.</remarks>
  29:  public Image GetImagenConMarco(byte[] bytes)
  30:  {
  31:      Image img = JnSoftware.Utiles.GestionImagenes.byteArrayToImage(bytes);
  32:      return GetImagenConMarco(img);
  33:  }
  34:  
  35:  
  36:  /// <summary>
  37:  /// Encuadra la imagen dentro de un marco
  38:  /// </summary>
  39:  /// <param name="imagen">Imagen que quiere encuadrarse</param>
  40:  /// <returns>
  41:  /// Imagen encuadrada y sin distorsionar con el mayor tamaño posible
  42:  /// </returns>
  43:  public Image GetImagenConMarco(Image imagen)
  44:  {
  45:      imagenOriginal = imagen;
  46:      adaptaImagenACuadro();
  47:      return fondo;
  48:  }
  49:  
  50:  private Image imagenOriginal;

 

Se puede apreciar cómo el único método que realmente hace algo con respecto de la imagen es el último y, sencillamente, queda delegado al método privado adaptaImagenACuadro().

   1: /// <summary>
   2: /// Gestor de acciones.
   3: /// </summary>
   4: private void adaptaImagenACuadro()
   5: {
   6:     creaEntornoTrabajo();
   7:     estableceFondo();
   8:     creaMarco();
   9:     insertaImagen();
  10: }

Crear una nueva imagen como fondo

Creación de una nueva imagen con el tamaño especificado a la vez que la dejamos preparada para poder trabajar con GDI+.

   1: /// <summary>
   2:  /// Crea el fondo con el tamaño indicado.
   3:  /// </summary>
   4:  private void creaEntornoTrabajo()
   5:  {
   6:      // Creamos el fondo
   7:      fondo = new Bitmap(RecuadroExterior.Width, RecuadroExterior.Height);
   8:      entorno = Graphics.FromImage(fondo);
   9:  }
  10:  
  11:  private Image fondo;
  12:  Graphics entorno;

Rellenamos el fondo de color blanco que, aunque en el ejemplo es fijo, se puede cambiar fácilmente. La metodología: dibujar un rectángulo lleno que cubra toda la superficie.

   1: /// <summary>
   2: /// Crea el fondo de la imagen
   3: /// </summary>
   4: private void estableceFondo()
   5: {
   6:     Point posInicial = new Point(0, 0);
   7:     Brush colorFondo = Brushes.WhiteSmoke;
   8:  
   9:     Rectangle r = new Rectangle(posInicial, RecuadroExterior);
  10:     entorno.FillRectangle(colorFondo, r);
  11: }

Para el recuadro exterior optamos por usar la misma técnica, es decir, crear tantos recuadros no llenos en los que cada uno de ellos tiene un borde de color diferente que va desde oscuro a claro. De esta manera obtendremos un efecto de degradado.

   1: /// <summary>
   2: /// Crea un marco degradado en base a rectángulos.
   3: /// </summary>
   4: private void creaMarco()
   5: {
   6:     // Añadimos el marco
   7:     int intervalo = 255 / CapasDegradado;
   8:     for (int i = 0; i < CapasDegradado; i++)
   9:     {
  10:         Rectangle r = new Rectangle();
  11:         r.X = i;
  12:         r.Y = i;
  13:         r.Width = RecuadroExterior.Width - (i * 2);
  14:         r.Height = RecuadroExterior.Height - (i * 2);
  15:  
  16:         Pen p = new Pen(Color.FromArgb(i * intervalo, i * intervalo, i * intervalo));
  17:         entorno.DrawRectangle(p, r);
  18:     }
  19: }

Tratamiento de la imagen original

Tal como comentaba al principio, en el caso que la imagen pasada como parámetro sea inferior al recuadro, tan sólo será cuestión de centrarla en esta nueva imagen que nos hemos creado como fondo.

   1: /// <summary>
   2: /// Redimensiona la imagen y la centra
   3: /// </summary>
   4: private void insertaImagen()
   5: {
   6:     Size tamanoImagen = imagenOriginal.Size;
   7:  
   8:     // Se comprueba si la imagen es menor que el marco
   9:     if (!((tamanoImagen.Width < RecuadroExterior.Width - CapasDegradado * 2)
  10:      && (tamanoImagen.Height < RecuadroExterior.Height - CapasDegradado * 2)))
  11:     {
  12:         redimensionaImagen();
  13:     }
  14:     centraImagen();
  15: }
  16:  
  17:  
  18: // <summary>
  19: /// La imagen ya está redimensionada a un tamaño más pequeño 
  20: /// que el cuadro. 
  21: /// Tan sólo se trata de centrarla.
  22: /// </summary>
  23: private void centraImagen()
  24: {
  25:     Point posInicial = new Point();
  26:     posInicial.X = (RecuadroExterior.Width - imagenOriginal.Width) / 2;
  27:     posInicial.Y = (RecuadroExterior.Height - imagenOriginal.Height) / 2;
  28:  
  29:     Rectangle r = new Rectangle(posInicial, imagenOriginal.Size);
  30:     entorno.DrawImage(imagenOriginal, r);
  31: }

Hasta el momento, la clase será operativa en el caso que la imagen sea inferior en tamaño al recuadro deseado. Por lo tanto, tan sólo nos quedará redimensionar la imagen en el caso que tenga un tamaño superior. Para que el redimensionado sea lo más efectivo posible lo que hago es mirar la proporción ancho-alto de la imagen y comparar la longitud que sea superior. De esta manera, obtendremos el mayor tamaño posible sin que se deforme la misma.

   1: /// <summary>
   2: /// La imagen es mayor que el cuadro de presentación, así que se 
   3: /// se redimensiona para que se adapte, sin deformarse al ancho/alto 
   4: /// mayor
   5: /// </summary>
   6: private void redimensionaImagen()
   7: {
   8:     double proporcionAncho;
   9:     double proporcionAlto;
  10:     double factorReduccion;
  11:     Size s = imagenOriginal.Size;
  12:  
  13:     proporcionAncho = (double)imagenOriginal.Width / (double)(RecuadroExterior.Width - CapasDegradado * 2);
  14:     proporcionAlto = (double)imagenOriginal.Height / (double)(RecuadroExterior.Height - CapasDegradado * 2);
  15:  
  16:     if (proporcionAncho > proporcionAlto)
  17:         factorReduccion = proporcionAncho;
  18:     else
  19:         factorReduccion = proporcionAlto;
  20:  
  21:     imagenOriginal = new Bitmap(imagenOriginal, (int)(s.Width / factorReduccion), (int)(s.Height / factorReduccion));
  22: }

 

Por si acaso se ha quedado algo en el tintero a la hora de hacer los Copy&Paste del código dejo un ejemplo que muestra la funcionalidad en un entorno WinForms.

Enlace

SSMS: No se permite guardar los cambios

Igual el post actual causa algún tipo de controversia entre los DBA, y no existe nada más lejos de mi intención. Para un desarrollador como yo, si intentas hacer un cambio en una tabla de SQL Server (actualmente estoy con la versión 2008 R2) y el entorno te da un problema diciendo que no puedes hacerlo, realmente es un fastidio.

Jesús, ¿de qué estás hablando?. Pues realmente es algo que es muy sencillo, pero que es bastante molesto cuando intentas evitarte problemas a la hora de desarrollar. Pongamos un ejemplo, que así seguro que se entiende mucho mejor.

Tenemos que crear una tabla, por ejemplo, de Categorías. Como tan sólo se trata de crear la base de datos en local para simular el entorno de producción, hacemos uso del SSMS. Como es normal en un desarrollador despistado, la tabla resultante es algo similar a lo siguiente:

image

¿Qué ha pasado?. Algo muy sencillo. Al ser una tabla auxiliar, no he configurado el campo clave de la tabla como IDENTITY, así que, usando también el SSMS me dispongo a modificar la propiedad Especificación de identidad y cuál no será mi sorpresa al ver el siguiente mensaje:

image

Vale… ahora casi empiezo a acordarme que mi profe de BBDD nos comentaba que, a nivel interno, cuando se modificaba un campo se eliminaba éste y se volvía a crear. Vuelvo a repetir, si algún DBA me lleva la contraria, seguro que tiene razón, pero yo lo entendí más o menos así. De todas maneras, tenga razón o no, para mí es un fastidio tener que echar mano del SQL para crear un nuevo campo, traspasar los datos y convertir el nuevo campo en PK eliminando la PK actual… Ufff. Si tan sólo estamos hablando de un entorno de desarrollo…

Resulta que el problema del mensajito no es de SQL sino del SMSS, que tiene una prevención de cambios que supongo que será muy lógica en entornos de producción pero bastante molesta para nosotros, los desarrolladores. En la versión 2008 (es la que utilizo, no sé el resto) existe una opción en Herramientas, Opciones, Diseñadores que, por defecto viene activada y que nos impide realizar ciertos cambios en las tablas. Bastará con desmarcar la opción Impedir guardar cambios que requieran volver a crear tablas para que podamos, por ejemplo, convertir un campo “despistado” a un campo IDENTITY.

image

Leer variables Session desde una capa de Negocios

En una reciente conversación en los foros de ms, desempolvé una vieja utilidad que en más de una ocasión me ha salvado de un apuro y que, aunque no tenía en el olvido, reconozco que no le di la importancia que tenía hasta que comprendí que mucha gente tiene los mismos problemas que tengo yo. Al fin y al cabo siempre pienso que soy más torpe que los demás y que lo que a mí me cuesta mucho rato desarrollar, el resto lo resuelven a la primera. Bueno, intento comentarlo con un ejemplo, que siempre es más ilustrativo:4

Una aplicación con dos capas y media (realmente son tres, pero como no viene al caso, intento abstraerme de la capa de datos). Una capa de presentación separada físicamente de la capa de negocios, es decir, en proyecto diferente. Con una aplicación a medio crear, de buenas a primeras nos damos cuenta que será mejor almacenar en tablas quién es el usuario que ha tocado el registro y cuál es la fecha de dicho cambio. Con respecto a la fecha no existe mucho problema pero si nos fijamos en el nombre del usuario apreciamos que está en la capa de presentación y no llega a la capa de negocios. ¿Qué hacemos en este caso?. Una de las opciones que no nos gusta a ningún desarrollador es la de modificar todos los procedimientos de datos, incluyendo el nombre del usuario en ellos. Con esta opción, seguro que nos dejamos alguna página o procedimiento sin actualizar…

¿Qué opción debemos tomar entonces?. Algo tan sencillo como hacer uso de la variable Session desde la capa de negocios. En algún foro creo haber leído el concepto de “Encapsular las variables Session” o “Añadir Intellisense a las variables de Session”. No sé cuál sería la nomenclatura adecuada pero creo que ambas tienen su parte de razón. Veamos el código:

   1: /// <summary>
   2: /// Obtiene o establece el nombre del usuario actual.
   3: /// </summary>
   4: public static string UserWeb
   5: {
   6:     get
   7:     {
   8:         if (System.Web.HttpContext.Current.Session["userName"] == null)
   9:             System.Web.HttpContext.Current.Session["userName"] = "unknown";
  10:         return System.Web.HttpContext.Current.Session["userName"].ToString();
  11:     }
  12:     set { System.Web.HttpContext.Current.Session["userName"] = value; }
  13: }

Si esta propiedad la introducimos dentro de la capa de negocios (se sobreentiende que en un proyecto diferente de la de presentación) podremos conseguir un “intellisense” de las variables de Session a la vez que estamos “encapsulando” las variables de sesión. Se cual sea su nomenclatura, nos servirá para poder tener una misma variable desde la capa de presentación como desde la capa de negocios que, al fin y al cabo era el objetivo buscado.

Internacionalizar las fechas (o parte de ellas)

Nunca dejará de sorprenderme la librería de clases del .Net Framework. Ahora que andamos liados con el tema de internacionalización de un par de aplicaciones se nos presentaba la problemática de tener que mostrar, en algún momento, algún que otro control en el que se tiene que, por ejemplo, rellenar un combo con los meses del año en el idioma del usuario. Comúnmente, cuando he hecho esto con anterioridad siempre me he creado algún que otro método en la capa de negocios que devolviese los meses, los meses abreviados, los días…

Si hablamos de aplicaciones en los que sólo interviene un idioma recuerdo haber creado casi siempre un método que devuelva los meses del año. Algo así como:

   1: public string[] GetMeses1()
   2:       {
   3:           string[] meses = 
   4:           {
   5:               "Enero", "Febrero", "Marzo",
   6:               "Abril", "Mayo", "Junio",
   7:               "Julio", "Agosto", "Septiembre",
   8:               "Octubre", "Noviembre", "Diciembre"
   9:           };
  10:           return meses;
  11:       }

Realmente es sencillo el método y, quizás por eso, muchos desarrolladores prefieren escribir algún algoritmo para evitar teclear todos y cada uno de los meses del año, así que éste podría ser un buen sustituto

   1: public string[] GetMeses2()
   2: {
   3:     string[] meses = new string[12];
   4:  
   5:     // Se obtiene un día cualquiera del primer mes de cualquier año
   6:     DateTime primerMes = new DateTime(1900, 1, 1);
   7:     for (int i = 0; i < 12; i++)
   8:     {
   9:         // Se suman los meses a la fecha inicial
  10:         meses[i] = primerMes.AddMonths(i).ToString("MMMM");
  11:     }
  12:     return meses;
  13: }

Hasta ahora esto ha sido lo sencillo y lo que en más de una ocasión hemos usado todos. La entrada de aplicaciones con necesidades de internacionalización nos haría pensar, si aun estamos usando el primero de los métodos en algún tipo de estructura if o switch o algo del siguiente estilo:

   1: public string[] GetMeses3(System.Globalization.CultureInfo culture)
   2: {
   3:     string[] meses = new string[12];
   4:  
   5:     // Se obtiene un día cualquiera del primer mes de cualquier año
   6:     DateTime primerMes = new DateTime(1900, 1, 1);
   7:     for (int i = 0; i < 12; i++)
   8:     {
   9:         // Se suman los meses a la fecha inicial
  10:         meses[i] = primerMes.AddMonths(i).ToString("MMMM",culture);
  11:     }
  12:     return meses;
  13: }

Como solución rápida no está mal del todo, no obstante debemos tener en cuenta que, además de los meses, en las aplicaciones acabamos usando listas con, por ejemplo, los días de la semana. ¿Qué hacemos ahora?. ¿Creamos un método que nos busque un lunes y calcule hasta el domingo? ¿Qué pasa con los lugares en los que el primer día de la semana es el domingo en vez de el lunes?. El caso es que buscando algún tipo de respuesta a estas preguntas me he encontrado con una propiedad de la clase CultureInfo realmente interesante, concretamente la propiedad DateTimeFormat que nos devuelve una instancia de la clase DateTimeFormatInfo, la cual contiene propiedades muy interesante que nos facilitarán enormemente el trabajo. Un ejemplo de esto puede ser un método que nos devuelva la misma información que cualquiera de los métodos mostrados con anterioridad:

   1: public string[] GetMeses4(System.Globalization.CultureInfo culture)
   2: {
   3:     return culture.DateTimeFormat.MonthNames;
   4: }

No obstante, la clase DateTimeFormatInfo nos ofrece una serie de métodos y propiedades que nos puede ser muy útil cada vez que necesitemos echar mano de, por ejemplo:

  • DayNames, que obtiene los nombres de los días de la semana.
  • ShortestDayNames, que obtiene una matriz con los nombres abreviados de la semana.
  • GetDayName. Método que nos permite obtener el nombre de un día determinado
  • GetMonthName. Ídem con el nombre de un mes concreto.

C#. Unidades de capacidad

No será la primera vez que hemos creado algún sistema de visualización de archivos y, casi siempre solemos poner lo mismo, es decir, un grid en el que aparecen datos como Nombre, Extensión, Tamaño… Hoy me he fijado en el tamaño.

La magnífica clase System.IO.FileInfo nos proporciona casi todos los elementos a los que solemos hacer referencia cuando trabajamos con algún que otro archivo. Las propiedades Name, FullName, Extension y Length quizás sean los elementos más socorridos a la hora de mostrar algún que otro grid con los datos. El “problema” que le veo es que la longitud del fichero siempre nos lo está devolviendo en una misma medida, en bytes.

grid_bytes

 

¿Qué pasa si no quiero que se muestre así?. En un caso particular que me lleva ahora, al tratarse de ficheros bastante grandes, he preferido mostrar las unidades bastante abreviadas, del tipo 20,23 Mb o 34,21 Kb. Algo así como en el siguiente ejemplo:

grid_medidas

 

Bueno, supongo que el ejemplo queda claro, aunque también supongo que habrá alguien que deje el comentario refiriéndose que es más cómodo para ordenar y para hacerse una idea en una misma medida que en varias. ¡Vale!,.es el riesgo de publicar algo en Internet pero… para gustos, colores.

Vamos al grano, es decir, a crear una clase en C# que convierta una determinada cantidad de bytes en cualquier otra más abreviada. Al principio de esta labor, rememorando las clases de fundamentos de programación estuve tentado de hacer algún tipo de algoritmo de búsqueda para poder encontrar cuál era el número por el que podíamos dividir los bytes para que no fuesen 0 que, al fin y al cabo, es la única dificultad que estriba este módulo. Al final veo que también las clases de análisis en las que me intentaban explicar la funcionalidad de los logaritmos dejaron algún tipo de mella en mí.

Manos a la obra.

Necesito crear algún tipo de método en el que, pasándole un número de bytes como parámetro, nos devuelva una unidad abreviada. Así, si pasamos, por ejemplo, 2048 Bytes, el resultado que me devolverá será 2 Kb. En principio, la idea es crear un método estático del tipo.

   1: public static string GetFileSize(long numBytes)
   2: {
   3:     // TODO : Pendiente de implementar 
   4: }

Aunque, al empezar a crear el método, apercibo que será mucho mejor poder especificar desde la capa de presentación el formato. En este apartado no sé si se hace así o hubiese sido mejor echar mano de la UICulture, pero por comodidad he optado por hacer una pequeña “trampilla”. En este caso, la bendita sobrecarga ofrece la posibilidad de especificar el formato siguiendo las normas del formato compuesto, siendo {0} la cantidad y {1} la unidad de medida.

 

   1: public static string GetFileSize(long numBytes)
   2: {
   3:     return GetFileSize(numBytes, "{0:N0} {1}");
   4: }
   5:  
   6: public static string GetFileSize(long numBytes, string format)
   7: {
   8:     // TODO : Pendiente de implementar
   9: }

 

Una vez establecido el inicio y resuelto el problema de cómo localizar la unidad de un modo correcto, sólo nos queda el desarrollo del resto de la clase:

   1: using System;
   2:  
   3:  
   4:  
   5: namespace JnSoftware.Utiles
   6: {
   7:  
   8:     /// <summary>
   9:     /// Servicios de utilidad general de trabajo con ficheros
  10:     /// </summary>
  11:     public class Ficheros
  12:     {
  13:  
  14:         /// <summary>
  15:         /// Posibles capacidades de un archivo
  16:         /// </summary>
  17:         public enum CapacidadEnum
  18:         {
  19:             /// <summary>
  20:             /// Byte
  21:             /// </summary>
  22:             Byte = 0,
  23:             /// <summary>
  24:             /// Kilobyte
  25:             /// </summary>
  26:             Kb = 1,
  27:             /// <summary>
  28:             /// Megabyte
  29:             /// </summary>
  30:             Mb = 2,
  31:             /// <summary>
  32:             /// Gigabyte
  33:             /// </summary>
  34:             Gb = 3,
  35:             /// <summary>
  36:             /// Terabyte
  37:             /// </summary>
  38:             Tb = 4,
  39:             /// <summary>
  40:             /// Petabyte
  41:             /// </summary>
  42:             Pt = 5,
  43:             /// <summary>
  44:             /// Exabyte
  45:             /// </summary>
  46:             Ex = 6,
  47:             /// <summary>
  48:             /// Zetabyte
  49:             /// </summary>
  50:             Zb = 7
  51:         }
  52:  
  53:         private const long baseExp = 1024; // 2 ^10
  54:  
  55:         /// <summary>
  56:         /// Obtiene el valor de la medida en la que nos encontraremos
  57:         /// con valores diferentes de 0.
  58:         /// </summary>
  59:         /// <param name="numBytes">Bytes del fichero</param>
  60:         private int getPotencia(long numBytes)
  61:         {
  62:             return (int)Math.Log(numBytes, baseExp);
  63:         }
  64:  
  65:  
  66:         /// <summary>
  67:         /// Obtiene la unidad de capacidad
  68:         /// </summary>
  69:         /// <param name="potencia">Valor de la medida en la que nos
  70:         /// encontramos con valores diferentes de 0</param>
  71:         private string getUnidadCapacidad(int potencia)
  72:         {
  73:             return Enum.GetName(typeof(CapacidadEnum), potencia);
  74:         }
  75:  
  76:  
  77:  
  78:         private string getSizeAndUnity(long numBytes, string formatoNumerico)
  79:         {
  80:             int pot = getPotencia(numBytes);
  81:             double valor;
  82:             if (pot != 0)
  83:                 valor = numBytes / (Math.Pow(baseExp, pot));
  84:             else
  85:                 valor = numBytes;
  86:  
  87:             string unidad = getUnidadCapacidad(pot);
  88:             return string.Format(formatoNumerico, valor, unidad);
  89:         }
  90:  
  91:  
  92:  
  93:         /// <summary>
  94:         /// Estandariza el tamaño pasado como parámetro
  95:         /// en unidad de capacidad informática
  96:         /// </summary>
  97:         /// <param name="numBytes">Bytes del fichero</param>
  98:         /// <returns>
  99:         /// Cadena de texto en formato "{0:N0} {1}", siendo el 
 100:         /// primer parámetro el número de bytes y el segundo parámetro
 101:         /// la unidad de capacidad
 102:         /// </returns>
 103:         public static string GetFileSize(long numBytes)
 104:         {
 105:             return GetFileSize(numBytes, "{0:N0} {1}");
 106:         }
 107:  
 108:  
 109:  
 110:         /// <summary>
 111:         /// Estandariza el tamaño pasado como parámetro
 112:         /// en unidad de capacidad informática
 113:         /// </summary>
 114:         /// <param name="numBytes">Bytes del fichero</param>
 115:         /// <param name="format">Cadena de formato compuesto.
 116:         ///     {0} = Cantidad numérica
 117:         ///     {1} = Unidad de medida
 118:         /// </param>
 119:         /// <returns>
 120:         /// Cadena de texto en formato "{0:N0} {1}", siendo el 
 121:         /// primer parámetro el número de bytes y el segundo parámetro
 122:         /// la unidad de capacidad</returns>
 123:         public static string GetFileSize(long numBytes, string format)
 124:         {
 125:             Ficheros f = new Ficheros();
 126:             return f.getSizeAndUnity(numBytes, format);
 127:         }
 128:  
 129:  
 130:  
 131:     }
 132: }

Observaciones

Imagino que alguien me comentará que me he dejado el YottaByte en la enumeración. ¡Pues vale!, Pasando de los Mb, el resto lo puse más porque me hacía gracia que por la utilidad. Hasta el momento no suelo estar habituado a trabajar con ficheros que superen los Tb.

También me anduve mirando otro punto un poco delicado, referente a los prefijos binarios, sobretodo, por las diferencias existentes entre las medidas de discos y las estándares y, al final he optado por seguir la unidad estándar. Al fin y al cabo, la necesidad inicial era la de trabajar con ficheros almacenados en un servidor Web… No sé si realmente notaré mucho las diferencias hablando de mebis o megas…

LastUpdateUser y LastUpdateDate en capa de negocios

Desde los primeros programitas que desarrollé, algo que me ha gustado tener y que, en más de una ocasión me ha servido para descubrir algún que otro problemilla ha sido guardar en las tablas de la base de datos los campos LastUpdateUser y LastUpdateDate con la finalidad de almacenar el usuario y la fecha de la última modificación. Este proceso, ya desde los tiempos de VB5 era un proceso bastante sencillo desde cualquier apartado de la aplicación, fuese un fichero .EXE o una .DLL. En principio, si la memoria no me falla, sólo había que hacer una llamada a la función GetUserName de la librería advapi32.dll.

Ha cambiado todo bastante desde aquellos tiempos y, lo más importante, ha cambiado el entorno de trabajo. Ahora ya no hace falta hacer llamadas a las API, o mejor dicho, sí pero muchísimo más sencillo que antaño. En el namespace System nos encontramos con una clase muy útil, Environment que incluye entre otras cosas, una propiedad UserName que nos devuelve el nombre de usuario que ha iniciado la sesión en Windows. Hasta aquí está todo bien, siempre que estemos trabajando en un entorno WinForms y siempre que la aplicación completa esté siendo ejecutada en la máquina del cliente.

En ASP.NET el problema es bien distinto dependiendo del escenario y realmente es uno bastante frecuente puesto que pocas son las aplicaciones ya que incluyen en el mismo proyecto la lógica de negocios y de presentación juntas.

¿Dónde nos encontramos realmente el problema?. Como siempre, intento explicarlo con un ejemplo. Pongamos que tenemos una solución con dos proyectos: Web como capa de presentación y BAL como capa de negocios. Ya tenemos montados (y funcionando) todo el tema de membresías y usuarios para poder acceder. Desde la Web sí que podemos tener el nombre del usuario actual

   1: string usuarioActual = User.Identity.Name;

pero sólo desde la web. Si necesitamos tener el mismo dato desde la capa de negocios, deberíamos ir pasando este dato como parámetro en cualquiera de los métodos que alteren datos.

   1: GuardaDatos(dato1,dato2,...,usuarioActual);

¿No quedaría mucho mejor que este dato lo tuviésemos ya en la capa de negocio para hacer algo similar a lo siguiente?:

   1: public void GuardaDatos(string dato1, int dato2)
   2: {
   3:     ...
   4:     c.Dato1 = dato1;
   5:     c.Dato2 = dato2;
   6:     c.LastUpdateUser = xxxx;
   7:     c.LastUpdateDate = System.DateTime.Now;
   8:     ...
   9: }

El problema nos lo encontramos en lo que he denominado xxxx en el ejemplo, aunque para renombrarlo un poco, vamos a darle un nombre diferente creando, por ejemplo, una propiedad estática en una nueva clase a la que denominaremos Configuracion.

   1: namespace BAL
   2: {
   3:     public class Configuracion
   4:     {
   5:         private static string _usuario = "DESCONOCIDO";
   6:  
   7:         /// <summary>
   8:         /// Obtiene o establece el usuario identificado en la aplicación Web
   9:         /// </summary>
  10:         public static string UsuarioActual
  11:         {
  12:             get { return _usuario; }
  13:             set { _usuario = value; }
  14:         }
  15:     }
  16: }

Ahora, con esta clase, lo único que nos queda es establecer el valor de la propiedad UsuarioActual. Para ello, tendremos que crearnos un clase de aplicación global, es decir, el fichero Global.asax que nos permitirá capturar el evento AuthenticateRequest de la clase HttpApplication. De esta manera, cada vez que se realice una solicitud sobre cualquier página, estaremos estableciendo el valor de la propiedad UsuarioActual con el nombre del usuario (en el caso que haya sido satisfactoria la autenticación).

   1: protected void Application_AuthenticateRequest(object sender, EventArgs e)
   2: {
   3:     BAL.Configuracion.UsuarioActual =
   4:       Request.IsAuthenticated
   5:       ? User.Identity.Name
   6:       : "DESCONOCIDO";
   7: }

¡Cómo me gusta el operador ternario de C#!. Aprendí a usarlo en Java y creo que es de lo más cómodo.

C#. NombrePropio

Lamento tener tan abandonado el blog pero es que ando inmerso en un proyecto de aquellos que te absorben mucho más tiempo del que te gustaría. Intentaremos sacar algo de tiempo ahora que, por lo menos ya no tengo clases.

Bueno, hoy le toca el turno a una de aquellas pequeñas cosas a las que, los que vais siguiendo el blog ya estáis acostumbrados. El caso es que el otro día, charlando con el jefe me comenta lo mal que queda que los clientes vean los datos unos en mayúscula, otros en formato Título, otros en minúscula… Sí, vale, tiene razón, queda feo. Con la naturalidad que me caracteriza le digo que no hay problema, que le pongo todo en mayúsculas y que así no hay diferencias pero no tarda en comentarme que eso es antiguo, que lo quiere en Nombre Propio, al igual que la función NOMPROPIO/PROPER del Excel.

Ale… para la “validación” de cualquier dato de texto, nos encontramos con los métodos ToUpper() y ToLower() de la clase System.String, pero no existe un ToNombrePropio() pues intentaremos crearlo nosotros.

El módulo en sí no es complicado puesto que la clase CultureInfo tiene una propiedad TextInfo que nos permitirá acceder a un método ToTitleCase() que ya hace precisamente lo que andábamos buscando.

Por tanto, lo único que nos queda es ponernos manos a la obra intentando seguir las mismas directrices que siguen los métodos ToLower() o ToUpper(), es decir, crear un método sobrecargado que nos permita, en caso necesario, pasar la cultura deseada.

   1: using System.Globalization;
   2: using System.Threading;
   3:  
   4: namespace System
   5: {
   6:     public static class StringExtensors
   7:     {
   8:         /// <summary>
   9:         /// Devuelve una copia de este objeto String en formato Título aplicando
  10:         /// las reglas de la cultura actual
  11:         /// </summary>
  12:         public static string ToNombrePropio ( this System.String  texto )
  13:         {
  14:             return texto.ToNombrePropio(Thread.CurrentThread.CurrentCulture);
  15:         }
  16:  
  17:         /// <summary>
  18:         /// Devuelve una copia de este objeto String en formato título aplicando
  19:         /// las reglas de la cultura especificada
  20:         /// </summary>
  21:         /// <param name="culture">Objeto System.Globalization.CultureInfo que proporciona
  22:         /// reglas de Titulo</param>
  23:         public static string ToNombrePropio( this System.String texto, CultureInfo culture)
  24:         {
  25:             TextInfo ti = culture.TextInfo;
  26:             return ti.ToTitleCase(texto);
  27:         }
  28:     }
  29: }

 

Eso sí, cuando andaba documentándome en la ayuda de Microsoft sobre el método ToTitleCase(), he visto que aparece el siguiente mensaje:

Como se muestra anteriormente, el método ToTitleCase() proporciona un comportamiento de grafía arbitrario que no es necesariamente lingüísticamente correcto. Una solución correcta lingüísticamente requeriría reglas adicionales y el algoritmo actual es algo más sencillo y rápido.Nos reservamos el derecho para hacer esta API más lenta en el futuro

No obstante, en la versión 4.0 del Framework todavía no se ha modificado…

Generar fechas aleatorias con C#

En los cursos suelo utilizar datos casi siempre aleatorios y, últimamente me ha dado por hacer los ficheros con C#. Hasta aquí, bien, pero me pasó una cosa curiosa ayer. El caso es que necesitaba crear un par de columnas con una serie de fechas aleatorias y, de buenas a primeras me quedé delante del ordenador, pensando. Me quedé en blanco porque, sencillamente, no se me ocurrían más que cosas en las que seguro que me tiraba un buen rato para una tontería como esta. Obtener un año aleatorio, calcular un mes, usar el método DateTime.DaysInMonth para que no se produzcan excepciones al obtener fechas del tipo 31/11/2011.

¿Por qué, de vez en cuando nos pasa que perdemos el norte y nos complicamos tanto la vida?. Cuando tenía la tira de líneas escritas me di cuenta que podría hacer una adición de días a una fecha inicial.

Por supuesto, dejo la clase resultante, no la que empecé a hacer…

   1: using System;
   2:  
   3: namespace JnSoftware.Helpers
   4: {
   5:  
   6:     /// <summary>
   7:     /// Utilidades de fechas
   8:     /// </summary>
   9:     class DateUtils
  10:     {
  11:         Random seed = new Random(DateTime.Now.Millisecond);
  12:  
  13:         // Evita la instancia de la clase.
  14:         private DateUtils() { }
  15:  
  16:  
  17:         private DateTime[] getDates(int numDates, DateTime dateInit, DateTime dateEnd)
  18:         {
  19:             DateTime[] lst = new DateTime[numDates];
  20:             for (int i = 0; i < numDates; i++)
  21:             {
  22:                 // Obtenemos el intervalo de tiempo
  23:                 TimeSpan interval = dateEnd.Subtract(dateInit);
  24:                 // Se calcula el número de días
  25:                 int randomMax = (int)interval.TotalDays;
  26:                 // Se obtiene un número aleatorio
  27:                 long randomValue = seed.Next(0, randomMax);
  28:                 // Se le añade a la fecha inicial
  29:                 lst[i] = dateInit.AddDays(randomValue);
  30:             }
  31:             return lst;
  32:         }
  33:  
  34:  
  35:  
  36:  
  37:         #region Métodos públicos estáticos
  38:         /// <summary>
  39:         /// Obtiene una matriz de Fechas obtenidas de forma pseudo-aleatoria.
  40:         /// </summary>
  41:         /// <param name="numDates">Nº de Fechas deseadas</param>
  42:         /// <returns></returns>
  43:         public static DateTime[] GetRandomDate(int numDates)
  44:         {
  45:             return GetRandomDate(numDates, DateTime.MinValue, DateTime.MaxValue);
  46:         }
  47:  
  48:  
  49:         /// <summary>
  50:         /// Obtiene una matriz de Fechas obtenidas de forma pseudo-aleatoria.
  51:         /// </summary>
  52:         /// <param name="numDates">Nº de Fechas deseadas</param>
  53:         /// <param name="yearInit">Año de inicio del intervalo</param>
  54:         /// <param name="yearEnd">Año de fin del intervalo</param>
  55:         /// <returns></returns>
  56:         public static DateTime[] GetRandomDate(int numDates, int yearInit, int yearEnd)
  57:         {
  58:             return GetRandomDate(numDates, new DateTime(yearInit, 1, 1), new DateTime(yearEnd, 12, 31));
  59:         }
  60:  
  61:  
  62:         /// <summary>
  63:         /// Obtiene una matriz de Fechas obtenidas de forma pseudo-aleatoria.
  64:         /// </summary>
  65:         /// <param name="numDates">Nº de Fechas deseadas</param>
  66:         /// <param name="dateInit">Fecha de inicio del intervalo</param>
  67:         /// <param name="dateEnd">Fecha de fin del intervalo</param>
  68:         /// <returns></returns>
  69:         public static DateTime[] GetRandomDate(int numDates, DateTime dateInit, DateTime dateEnd)
  70:         {
  71:             DateUtils du = new DateUtils();
  72:             return du.getDates(numDates, dateInit, dateEnd);
  73:         }
  74:  
  75:         #endregion
  76:     }
  77: }

Modificar valores de las propiedades de una clase

Estoy más que seguro que el post actual es una de aquellas cosas que casi todo el mundo conocía, pero a mí me ha dado una especie de subidón el descubrirlo. Explico el problema.

En un programa que estoy haciendo me he encontrado en la situación de que tengo que importar unos datos que vienen de una serie de proveedores. Hasta aquí ha sido un poco aburrido puesto que unos lo envían en formato .txt, otros en Access y, por supuesto, cada uno con su formato particular. Ya digo, aburrido, pero hasta el momento ha ido saliendo. En la implementación, lo que he hecho ha sido crearme una clase más o menos estándar con los datos que necesito y, sea cual sea el proveedor, relleno una lista con esa clase para poder hacer un tratamiento homogéneo.

  • Jesús, ¿para qué cuentas esta película?, ¿no puedes ir al grano?.
  • Ya sabes que me gusta ponerme en situación siempre
  • Si todo te va tan bien, ¿dónde está el problema entonces?

Pues el problema está en un proveedor concreto, que tiene una serie de variables que, aunque discretas, son indeterminadas en número. Intento explicarlo con un ejemplo que se asemeja mucho a la situación que me he encontrado.

Ejemplo
En nuestra aplicación tenemos, en la base de datos, las partidas de gastos que necesita nuestra empresa. A grandes rasgos, sería algo así como :

 

   1: public class Caja
   2: {
   3:     public double Personal { get; set; }
   4:     public double Maquinaria { get; set; }
   5:     public double GastosVarios { get; set; }
   6: }

Ahora nos envían un fichero en el que hay una serie de códigos los cuales, por supuesto, nada tienen que ver con nuestras partidas, así que se nos ocurre hacer una tabla en la base de datos para que sea el propio usuario el que cree una relación entre los códigos del proveedor y los de nuestra clase. En el código siguiente, para no andar a vueltas con la base de datos, he incluido un par de colecciones que permiten simular la lectura de dichos datos.

   1: class Aplicacion
   2:   {
   3:       public static void Main()
   4:       {
   5:           // Simulamos los datos que podríamos extraer de 
   6:           // una tabla de asignación
   7:           // Formato: <keyProveedor,keyDb>
   8:           Dictionary<string, string> tabla =
   9:              new Dictionary<string, string>();
  10:           tabla.Add("SUELDO", "Personal");
  11:           tabla.Add("CENA_NAVIDAD", "GastosVarios");
  12:           tabla.Add("PC", "Maquinaria");
  13:           tabla.Add("IMPRESORA", "Maquinaria");
  14:           tabla.Add("PROPINA_INFORMATICO", "GastosVarios");
  15:  
  16:           // Simulamos también un fichero leído del proveedor
  17:           // Formato: <keyProveedor,importe>
  18:           KeyValuePair<string, double>[] datos = new KeyValuePair<string,double>[5];
  19:           datos[0] = new KeyValuePair<string, double>("PC", 1200);
  20:           datos[1] = new KeyValuePair<string, double>("IMPRESORA", 600);
  21:           datos[2] = new KeyValuePair<string, double>("SUELDO", 1200);
  22:           datos[3] = new KeyValuePair<string, double>("SUELDO", 1500);
  23:           datos[4] = new KeyValuePair<string, double>("PROPINA_INFORMATICO", 4000);
  24:  
  25:           
  26:           Caja caja = new Caja();
  27:           
  28:           // Bucle por los valores importados 
  29:           foreach (KeyValuePair<string,double> kpv in datos)
  30:           {
  31:               // Se obtiene el equivalente proveedor->interno
  32:               string keyDb = tabla[kpv.Key];
  33:               // Se añade el valor a la clase Caja
  34:               caja.AddGasto(keyDb, kpv.Value);
  35:           }
  36:           Console.WriteLine(caja.ToString());
  37:           Console.ReadKey();
  38:       }
  39:   }

En el ejemplo es la colección tabla la que el usuario habría modificado de tal manera que relacionase los códigos internos nuestros con los del proveedor que nos suministra los datos. El resto, como se puede apreciar, es tan sólo un bucle por los datos leídos y la llamada a un módulo de la clase Caja que deberemos crear.

Ahora, en la clase Caja, tan sólo nos queda crear un par de módulos. Uno que permita leer el valor de una propiedad (getValue) y el otro, lógicamente, que permita escribir un valor en dicha propiedad (AddGasto).

   1: public class Caja
   2: {
   3:     #region Partidas presupuestarias
   4:     public double Personal { get; set; }
   5:     public double Maquinaria { get; set; }
   6:     public double GastosVarios { get; set; }
   7:     #endregion
   8:  
   9:  
  10:     /// <summary>
  11:     /// Añade el precio a la propiedad pasada como parámetro
  12:     /// </summary>
  13:     /// <param name="propiedad">Nombre de la propiedad de la clase</param>
  14:     /// <param name="importe">Valor que se quiere añadir</param>
  15:     public void AddGasto(string propiedad, double importe)
  16:     {
  17:         // Recupera el valor actual del importe
  18:         double importeActual = getValue(propiedad);
  19:  
  20:         // Establece el valor a la propiedad
  21:         PropertyInfo p = this.GetType().GetProperty(propiedad);
  22:         p.SetValue(this, importe + importeActual, null);
  23:     }
  24:  
  25:     /// <summary>
  26:     /// Obtiene el valor de la propiedad 
  27:     /// </summary>
  28:     /// <param name="propiedad">Nombre de la propiedad</param>
  29:     double getValue(string propiedad)
  30:     {
  31:         PropertyInfo p = this.GetType().GetProperty(propiedad);
  32:         return (double)p.GetValue(this, null);
  33:     }
  34:  
  35:     /// <summary>
  36:     /// Obtiene un array con las propiedades de la clase 
  37:     /// </summary>
  38:     string[] GetPropiedades()
  39:     {
  40:         return this.GetType().GetProperties().Select(p => p.Name).ToArray();
  41:     }
  42:  
  43:     public override string ToString()
  44:     {
  45:         StringBuilder sb = new StringBuilder();
  46:         foreach (string p in GetPropiedades())
  47:         {
  48:             sb.AppendFormat(" - {0},{1,10:N}n", p.PadRight(30, '.'), getValue(p));
  49:         }
  50:         return sb.ToString();
  51:     }
  52: }

Tal como comentaba al principio, es más que probable que sea el único que no había hecho hasta ahora uso del namespace System.Reflection, pero como nunca había tenido la necesidad de hacer un tipo de asistente de este estilo, ni me había preocupado.