Generar archivos Excel como un señor con ClosedXml

 

Anónimo
Venga, lo confieso: yo también he generado desde mis aplicaciones contenidos HTML y los he enviado al cliente en un archivo con extensión XLS, incluso modificando el content-type, para que pareciera un documento de hoja de cálculo. Durante años. Y también le he dicho a mis clientes que el molesto mensaje que aparece al abrirlo desde Excel, el que indica que el contenido del archivo no coincide con la extensión del mismo, es algo normal.

Pero esto se acabó desde que descubrí ClosedXML, un magnífico componente para .NET basado en el estándar OpenXML que permite la generación de archivos Excel “de verdad”, con formato, estilos, fórmulas, rangos, filtros, y casi todo lo que se nos pueda ocurrir.

ClosedXMLClosedXML, proyecto iniciado por Manuel de León y distribuido bajo licencia MIT, se aleja de la verbosidad y amplitud de alcance del Open XML SDK de Microsoft, ofreciendo un API mucho más natural e intuitivo exclusivamente diseñado para crear y manipular documentos Excel. De hecho, el nombre ClosedXML lo eligió después de conocer el SDK oficial y pensar “si es así como se trabaja con Open XML, preferiría utilizar algo que estuviera cerrado”, en referencia a la complejidad que el primero supone.

Dado que se basa en Open XML, para abrir los archivos generados se necesita Excel 2007 o una versión posterior, aunque creo que ocho años después de su aparición ya podríamos considerar que es un mínimo bastante razonable 😉

Instalación del componente

Como de costumbre, la instalación de ClosedXML la vamos a realizar a través de Nuget:

PM> Install-Package ClosedXML
Attempting to resolve dependency 'DocumentFormat.OpenXml (≥ 1.0)'.
Successfully installed 'ClosedXML 0.68.1'.
Successfully added 'DocumentFormat.OpenXml 1.0' to ClosedXmlDemo.Model.
Successfully added 'ClosedXML 0.68.1' to ClosedXmlDemo.Model.

No me canso de repetirlo: Nuget, ¿dónde has estado todos estos años? 😉

Creación y salvado de un documento Excel básico (Desktop, Webforms, MVC)

La creación y salvado a disco de un documento Excel es absolutamente trivial. Basta con instanciar un objeto de la clase XLWorkBook, añadir una nueva hoja su colección de Worksheets, e introducir valores en ella a través de su propiedad Cell, como podemos observar en el código genérico mostrado a continuación:

1
2
3
4
var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Sheet 1");
worksheet.Cell(1, 1).Value = "Hello, world!";
workbook.SaveAs("c:\temp\excel.xlsx");

De esta forma tan simple, en c:tempexcel.xlsx tendremos lo siguiente:
Hoja de cálculo generada con ClosedXML
Si estamos desarrollando una aplicación web, cuando generamos un archivo Excel lo habitual es que lo enviemos al usuario como documento adjunto para que lo descargue y guarde en su equipo, lo que implica modificar el content-type y añadir un encabezado content-disposition.

En este caso de usar WebForms, el código a emplear es, poco más o menos, el siguiente:

1
2
3
4
5
6
7
8
9
10
11
Response.Clear();
Response.ContentType =
     "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
Response.AddHeader("content-disposition", "attachment;filename="Demo.xlsx"");
 
using (var memoryStream = new MemoryStream())
{
    workbook.SaveAs(memoryStream);
    memoryStream.WriteTo(Response.OutputStream);
}
Response.End();

Observad que no estamos salvando el workbook  sobre un MemoryStream, que luego volcamos al stream de salida para que viaje al cliente. Este doble paso, en cualquier caso bastante sencillo de implementar, se debe a que ClosedXML requiere para guardar el archivo que el stream de salida sea de avance y retroceso, y es algo que OutputStream no cumple.

En caso de tratarse de ASP.NET MVC, el código es prácticamente el mismo, aunque lo correcto sería implementarlo en un ActionResult personalizado:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ExcelResult: ActionResult
{
    private readonly XLWorkbook _workbook;
    private readonly string _fileName;
 
    public ExcelResult(XLWorkbook workbook, string fileName)
    {
        _workbook = workbook;
        _fileName = fileName;
    }
 
    public override void ExecuteResult(ControllerContext context)
    {
        var response = context.HttpContext.Response;
        response.Clear();
        response.ContentType = "application/vnd.openxmlformats-officedocument."
                             + "spreadsheetml.sheet";
        response.AddHeader("content-disposition",
                           "attachment;filename=""+ _fileName +".xlsx"");
 
        using (var memoryStream = new MemoryStream())
        {
            _workbook.SaveAs(memoryStream);
            memoryStream.WriteTo(response.OutputStream);
        }
        response.End();
    }
}

Y ya podríamos usarlo directamente desde un controlador:

1
2
3
4
5
6
7
8
public ActionResult GenerateExcel()
{
    // Generate the workbook...
    var workbook = ClosedXmlDemoGenerator.GenerateWorkBook();
 
    // ... and return it to the client
    return new ExcelResult(workbook, "demo");

Establecer valores en las celdas

Las celdas podemos referenciarlas utilizando la propiedad Cell de los objetos IXLWorksheet, y podemos hacerlo indicando su número de fila y columna o mediante el nombre usado normalmente en el mismo Excel:

1
2
worksheet.Cell(1, 1).Value = "Hello, world!";
worksheet.Cell("A2").Value = "How are you?";

Establecer valores de celdas con ClosedXML
Por supuesto, los valores pueden ser de todo tipo, de hecho la propiedad Value que establecemos es de tipo object, aunque obviamente sólo serán reconocidos los tipos habituales de Excel:

1
2
3
4
5
6
7
8
9
10
11
12
worksheet.Cell("A2").Value = "Text";
worksheet.Cell("B2").Value = "Hi!!";
worksheet.Cell("A3").Value = "Integer";
worksheet.Cell("B3").Value = 3;
worksheet.Cell("A4").Value = "Decimal";
worksheet.Cell("B4").Value = 3.5;
worksheet.Cell("A5").Value = "Boolean";
worksheet.Cell("B5").Value = true;
worksheet.Cell("A6").Value = "DateTime";
worksheet.Cell("B6").Value = DateTime.Now;
worksheet.Cell("A7").Value = "Object";
worksheet.Cell("B7").Value = new InvoiceDetails();

Tipos de datos en ClosedXML
También podemos crear rangos y establecerles valores de forma directa:

1
worksheet.Range("A1:D5").Value = "Hi!";

Valores de rangos en ClosedXML
E incluso podemos asignar directamente colecciones de datos como objetos de tipo DataTable o IEnumerable<T>:

1
2
3
4
5
6
worksheet.Cell("A1").Value = new[]
             {
                  new { Id=1, Name="John", Age = 42},
                  new { Id=2, Name="Peter", Age = 23},
                  new { Id=3, Name="Mary", Age = 32},
             };

Uso de conjuntos de datos en ClosedXML

Formato de celdas

ClosedXML pone a nuestra disposición un rico conjunto de propiedades y métodos para dar formato a las celdas o a rangos de ellas. La sintaxis fluida que podemos utilizar facilita mucho el descubrimiento de las posibilidades de formateo, y la implementación de un código muy limpio y comprensible:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
worksheet.Range("B2:E5")
         .SetValue("Hi!")
         .Style.Border.SetOutsideBorder(XLBorderStyleValues.Dotted);
 
worksheet.Range("B3:E5").Style
         .Font.SetFontSize(10)
         .Font.SetFontColor(XLColor.Gray)
         .Font.SetItalic(true);
 
worksheet.Range("B2:E2").Style
         .Font.SetFontSize(13)
         .Font.SetBold(true)
         .Font.SetFontColor(XLColor.White)
         .Fill.SetBackgroundColor(XLColor.Gray);

Formateo de celdas con ClosedXML
También podemos, por supuesto, establecer la alineación de celdas o rangos, unir celdas, o establecer el formato de visualización de sus valores. Vemos también, de paso, una forma más fluida de establecer los valores, usando SetValue():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
worksheet.Cell("A1")
         .SetValue(1234.56789)
         .Style.NumberFormat.SetFormat("#,##0.#0");
 
worksheet.Cell("A2")
         .SetValue(DateTime.Now)
         .Style.DateFormat.SetFormat("dd-mm-yyyy")
               .Alignment.SetVertical(XLAlignmentVerticalValues.Center);
 
worksheet.Range("A3:B4")
         .Merge()
         .SetValue("Merged cells")
         .Style.Alignment.SetHorizontal(XLAlignmentHorizontalValues.Center)
               .Alignment.TextRotation = 15; // Degrees

Formateo de celdas con ClosedXML

Uso de fórmulas

Esto es especialmente interesante, pues no había forma de conseguirlo cuando exportábamos archivos HTML, CSV o similares, en los que sólo importaba el valor de las celdas. Con ClosedXML tenemos total libertad para introducir fórmulas en las celdas; de esta forma, no sólo estaremos enviando al usuario un conjunto estático de datos, sino una completa hoja Excel que puede modificar (si nos interesa, claro) y usar para su trabajo:

1
2
3
4
5
6
7
8
9
10
11
12
13
worksheet.Cell("B2")
    .SetValue("Number")
    .Style.Font.SetBold(true);
 
for (int i = 1; i <= 5; i++)
{
    worksheet.Cell(3+i, 2).Value = i;
}
 
worksheet.Cell("A9").SetValue("Total");
worksheet.Cell("B9")
    .SetFormulaA1("=SUM(B3:B8)")
    .Style.Border.SetTopBorder(XLBorderStyleValues.Medium);

Fórmulas en ClosedXML

Otras características interesantes

ClosedXML soporta muchísimas funcionalidades adicionales a las descritas hasta el momento. Voy a citar algunas más que me han llamado la atención.

Por ejemplo, tenemos la posibilidad de añadir filtros y ordenación por columna a los datos, de manera que el usuario pueda realizar una selección de la información recibida:

1
2
3
4
5
6
7
8
9
10
11
worksheet.Cell("A1").Value = "Id";
worksheet.Cell("B1").Value = "Name";
worksheet.Cell("C1").Value = "Age";
worksheet.Cell("A2").Value = new[]
     {
          new { Id=1, Name="John", Age = 42},
          new { Id=2, Name="Peter", Age = 23},
          new { Id=3, Name="Mary", Age = 32},
          new { Id=4, Name="John", Age = 45},
     };
worksheet.RangeUsed().SetAutoFilter();

Filtros de datos con ClosedXML
Otro aspecto que puede ser interesante es la protección de celdas para que el usuario no pueda modificar sus valores. En el siguiente ejemplo, se protege la hoja completa con un password, de forma que no podrá ser editada, excepto las tres primeras columnas de la primera fila, que el usuario podrá editar con total libertad:

1
2
3
worksheet.Protect("1234"); // Locks the worksheet and sets the password
worksheet.Range("A1:C1")
    .Style.Protection.SetLocked(false); // Unlocks the range

Protección de hojas con ClosedXML
También, si nos interesa que el usuario edite valores de las celdas, es posible especificar restricciones en los datos de entrada, como su tipo, rango de valores permitidos, selección de valores desde desde un desplegable, etc.:

1
2
3
4
5
6
7
8
9
worksheet.Cell("A1").Value = "Digit 0-9:";
worksheet.Cell("B1").DataValidation.WholeNumber.Between(0, 9);
 
worksheet.Cell("A2").Value = "Two:";
worksheet.Cell("B2").DataValidation.WholeNumber.EqualTo(2);
 
worksheet.Cell("A3").Value = "Date:";
worksheet.Cell("B3").DataValidation.AllowedValues = XLAllowedValues.Date; // Only dates
worksheet.Cell("B3").DataValidation.ErrorMessage = "Only dates, please";

Validación de datos de entrada con ClosedXML
Incluso podemos añadir comentarios a celdas:

1
2
3
4
5
6
7
8
worksheet.Cell("B2").SetValue("TOTAL:")
    .Style.Font.SetBold(true)
          .Alignment.SetHorizontal(XLAlignmentHorizontalValues.Right);
 
worksheet.Cell("C2").Style.NumberFormat.SetFormat("#,##0.#0");
worksheet.Cell("C2").SetValue(9876543.21)
    .Comment.SetAuthor("jmaguilar")
    .AddText("Danger: this number seems odd!");

Añadir comentarios con ClosedXML

… y vamos a dejarlo aquí 😉

Bueno, pues tras este largo post espero que más o menos os haya quedado claro el uso y posibilidades de este magnífico componente, y que os hayan entrado muchas ganas de probarlo. Seguro cambiará la forma en que generáis los archivos Excel desde vuestras aplicaciones y abrirá nuevas posibilidades, hasta ahora difícilmente implementables usando otras alternativas.

También, recomendaros que no dejéis de leer la documentación, que es bastante extensa y detallada, donde podréis ver muchas más características que no he comentado para no hacer un post más interminable de lo que ya es 😉

Podéis descargar un proyecto de prueba desde mi Skydrive, donde veréis en funcionamiento la generación de archivos Excel desde una aplicación de consola, ASP.NET Webforms, y MVC.

Publicado en Variable not found.

ASP.NET MVC 4 y los nuevos atributos de validación de .NET 4.5

ASP.NET MVCUn post rapidito. Según puede consultarse en MSDN, ya tenemos confirmado que la versión 4.5 de .NET framework vendrá acompañada de un nuevo conjunto de atributos de validación para aplicar a las propiedades del Modelo en el espacio de nombres System.ComponentModel.DataAnnotations:

  • CreditCardAttribute, que puede ser utilizado para validar números de tarjeta de crédito.
  • EmailAddressAttribute, que validará direcciones de correo electrónico.
  • FileExtensionsAttribute, para validar extensiones en nombres de archivo.
  • PhoneAttribute, que indica cuándo una propiedad debe contener un número de teléfono válido.
  • UrlAttribute, que comprobará si el contenido de una propiedad es una URL válida.
  • CompareAttribute, que antes estaba disponible en System.Web.Mvc y ha “ascendido” a anotación de datos general, permite validar la igualdad de dos propiedades.

Pues bien, según se observa en este changeset del código fuente del framework, ya podemos asegurar que la versión final de MVC 4 incorporará adaptadores de cliente y servidor para que podamos utilizarlos de forma directa en nuestros desarrollos :-)

También se ha incluido soporte para el atributo MembershipPasswordAttribute, recientemente incluido en System.Web.Security, que comprueba si una contraseña cumple los requisitos establecidos para las mismas en el membership provider.

Publicado en Variable not found.

Validación manual con Data Annotations

Microsoft .NET Habitualmente asociamos la validación de entidades basadas en anotaciones de datos, o data annotations, a tecnologías como dynamic data o ASP.NET MVC, y estamos acostumbrados a que la validación se realice de forma automática, pero nada más lejos de la realidad. Podemos utilizar data annotations desde cualquier tipo de aplicación .NET (Webforms, Winforms, WPF, Consola, o cualquier otra en la que tengamos disponible System.ComponentModel.DataAnnotations), puesto que existe la posibilidad de invocar manualmente los procedimientos de validación.

En este post vamos a ver cómo realizar validaciones basadas en anotaciones de forma manual, lo cual puede tener su utilidad en gran número de escenarios.

Resumidamente, esta técnica consiste en decorar cada una de las propiedades con una serie de atributos llamados anotaciones (definidos en System.ComponentModel.DataAnnotations) que indican las comprobaciones que se aplicarán a la entidad para determinar su validez. La siguiente porción de código muestra una entidad en la que se están indicando estas restricciones en cada una de sus propiedades:

public class Friend
{
    [Required, StringLength(50)]
    public string Name { get; set; }
 
    [Range(0, 120)]
    public int Age { get; set; }
}

En el citado espacio de nombres encontramos atributos que cubren la mayoría de casos frecuentes: Required (propiedad obligatoria), RegularExpression (validar contra una expresión regular), StringLength (longitud máxima y mínima de un texto), Range (rangos de valores permitidos), y CustomValidation (validaciones personalizadas). Además, este conjunto de anotaciones puede ser extendido muy fácilmente creando atributos que hereden de ValidationAttribute, disponible también en System.ComponentModel.DataAnnotations.

Validación manual de objetos

De lo más sencillo: la clase estática Validator, disponible también en el namespace System.ComponentModel.DataAnnotations, ofrece métodos que permiten realizar las comprobaciones de forma directa sobre objetos o propiedades concretas.

En este caso, dado que lo que nos interesa es validar las entidades completas, utilizaremos el método Validator.TryValidateObject(), al que suministraremos:

  • el objeto a validar,
  • un contexto de validación (que debemos crear previamente),
  • una colección de ValidationResult en la que almacenaremos los errores,
  • y, por último, si deseamos validar todas las propiedades (indicando true), o por el contrario preferimos parar el proceso en cuanto se detecte el primer error (false).

La implementación de la validación podría ser como la que la sigue:

    private IEnumerable<ValidationResult> getValidationErrors(object obj)
    {
        var validationResults = new List<ValidationResult>();
        var context = new ValidationContext(obj, null, null);
        Validator.TryValidateObject(obj, context, validationResults, true);
        return validationResults;
    }

El método retornará una lista de errores vacía cuando el objeto haya superado las restricciones impuestas, o llena con los objetos ValidationResult que describen los problemas encontrados.

Y podríamos utilizarlo desde una aplicación de consola de la siguiente forma:

    var friend = new Friend { Age = -1, Name = "" };
    var errors = getValidationErrors(friend);
    foreach (var error in errors)
    {
        Console.WriteLine(error.ErrorMessage);
    }
The Name field is required.

The field Age must be between 0 and 120.

Los mensajes de validación que aparecen pueden ser definidos en la misma anotación, por ejemplo así:

    [Required(ErrorMessage="Please, enter the name")]
    public string Name { get; set; }

¿Y si los metadatos están en otra clase?

Hay escenarios en los que no tenemos acceso a la clase en la que deseamos introducir las anotaciones. Un ejemplo claro lo encontramos cuando nos interesa especificar las restricciones en una clase generada por un proceso automático, como el diseñador de EDM de Entity framework; cualquier cambio realizado sobre el código generado será sobrescrito sin piedad al modificar el modelo.

En estos casos, es una práctica frecuente definir los metadatos en clases “buddy”, que son copias exactas de la entidad a anotar, pero que serán utilizadas únicamente como contenedores de anotaciones. Las clases buddy se vinculan con la entidad original utilizando el atributo MetadataType de la siguiente forma:

    // This class has been generated by a tool
    public partial class Friend
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }
 
    // Let's associate the buddy class FriendMetadata
    [MetadataType(typeof(FriendMetadata))]
    public partial class Friend
    {
        
    }
 
    // Buddy class
    public class FriendMetadata
    {
        [Required]
        public string Name { get; set; }
 
        [Range(0, 120)]
        public int Age { get; set; }
    }

Observad que para poder utilizar esta técnica es necesario que la entidad a la que queremos añadir anotaciones sea creada como parcial. En caso contrario no podríamos indicarle con MetadataType dónde se encuentran definidos sus atributos de validación.

Pues bien, resulta que algunos marcos de trabajo (como ASP.NET MVC) están preparados para detectar este escenario y obtener de forma automática los metadatos desde la clase buddy, pero si estamos realizando la validación de forma manual el atributo [MetadataType] no será tenido en cuenta.

Por tanto, debemos ser nosotros los que indiquemos expresamente dónde se encuentran los metadatos, para lo que, afortunadamente, contamos con la ayuda de TypeDescriptor (definida en System.ComponentModel), desde donde podemos indicar el origen de los metadatos de clases simplemente registrando el proveedor desde el cual pueden ser obtenidos.

El procedimiento para conseguirlo es bastante simple: creamos un proveedor de descripciones basado en metadatos utilizando la clase AssociatedMetadataTypeTypeDescriptionProvider (uuf con el nombrecito ;-)) en el que vinculamos la clase “original” con la que contiene los metadatos (la clase buddy), y a continuación añadimos dicho proveedor a la primera.

Por ejemplo, para hacer que las anotaciones de la clase Friend se obtengan desde el tipo FriendMetadata podríamos incluir el siguiente código de inicialización:

var descriptionProvider = new AssociatedMetadataTypeTypeDescriptionProvider(
     typeof(Friend), 
     typeof(FriendMetadata)
);
TypeDescriptor.AddProviderTransparent(descriptionProvider, typeof(Friend));

Otra posibilidad más genérica sería implementarlo como se muestra a continuación, donde buscamos en todo el ensamblado actual clases decoradas con el atributo MetadataType, registrando el proveedor de metadatos indicado en dicho atributo de forma automática:

private static void registerBuddyClasses()
{
    var buddyAssociations = 
        from t in Assembly.GetExecutingAssembly().GetTypes()
        let md = t.GetCustomAttributes(typeof(MetadataTypeAttribute), false)
                .FirstOrDefault() as MetadataTypeAttribute
        where md != null
        select new { Type = t, Buddy = md.MetadataClassType };
 
    foreach (var association in buddyAssociations)
    {
        var descriptionProvider = 
            new AssociatedMetadataTypeTypeDescriptionProvider(
                association.Type, association.Buddy
            );
        TypeDescriptor.AddProviderTransparent(descriptionProvider, association.Type);
    }
}

De esta forma, bastará con invocar el método registerBuddyClasses() durante la inicialización de la aplicación para que las clases buddy sean registradas de forma automática.

Pero más interesante es, sin duda, que podríamos implementar nuevas fórmulas para indicar dónde se encuentran los metadatos de una clase. Por ejemplo, sería realmente sencillo modificar el método anterior para sustituir el atributo MetadataType por una convención de nombrado del tipo “las clases llamadas FooMetadata contendrán los metadatos de las clases de llamadas Foo”:

private static void registerBuddyClassesUsingConventions()
{
    var allAssemblyTypes = Assembly.GetExecutingAssembly().GetTypes().ToList();
    var buddyAssociations =
        from t in allAssemblyTypes
        let buddy = allAssemblyTypes
                    .FirstOrDefault(other => other.Name == t.Name + "Metadata")
        where buddy != null
        select new { Type = t, Buddy = buddy };
 
    foreach (var association in buddyAssociations)
    {
        var descriptionProvider =
            new AssociatedMetadataTypeTypeDescriptionProvider(
                association.Type, association.Buddy
            );
        TypeDescriptor.AddProviderTransparent(descriptionProvider, association.Type);
    }
}

¿Y si quiero usar IValidatableObject?

El interfaz IValidatableObject (definido System.ComponentModel.DataAnnotations) obliga a implementar un único método, llamado Validate(), que retornará una lista de objetos ValidationResult con los resultados de las comprobaciones.

A continuación se muestra un ejemplo de implementación de este interfaz sobre una entidad:

public class Friend : IValidatableObject
{
    public string Name { get; set; }
    public int Age { get; set; }
 
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Name.Equals("albert", StringComparison.CurrentCultureIgnoreCase))
        {
            yield return new ValidationResult("I don't like Alberts!");
        }
    }
}

El método Validate() impuesto por el interfaz será invocado automáticamente por el framework desde el mismo TryValidateObject() siempre que no encuentre errores al comprobar las restricciones especificadas mediante anotaciones. O sea, que sólo se invocará a Validate() cuando no se hayan detectado errores previos de validación.

En cualquier caso, si nos interesa validar de forma manual también estos objetos, siempre podemos hacerlo como sigue:

    private static IEnumerable<ValidationResult> getIValidatableErrors(object obj)
    {
        var validationResults = new List<ValidationResult>();
        var context = new ValidationContext(obj, null, null);
        var validatable = obj as IValidatableObject;
        if(validatable!=null) 
            validationResults.AddRange(validatable.Validate(context));
 
        return validationResults;
    }

De esta forma, podríamos comprobar la ejecución así:

    var friend = new Friend { Age = -1, Name = "albert" };
    var errors = getValidationErrors(friend);
    foreach (var error in errors)
    {
        Console.WriteLine(error.ErrorMessage);
    }
The field Age must be between 0 and 120.

I don’t like Alberts!

En resumen, en este post hemos visto cómo utilizar las herramientas que ofrece el framework .NET para trabajar con validaciones basadas en data annotations de forma manual, lo que abre su ámbito de utilización a prácticamente cualquier tipo de aplicación para este marco de trabajo. Por el camino hemos repasado los mecanismos de anotaciones, y diversos escenarios como la externalización de atributos en clases buddy o el uso de la interfaz IValidatableObject.

Descargar un proyecto VS2010 con el código y pruebas desde Skydrive.

Publicado en Variable not found.

Parámetros opcionales: úsense con precaución

Los parámetros opcionales son una interesante ayuda que hace tiempo que está presente en otros lenguajes como Visual Basic .NET, y ha sido introducida en la versión 4.0 de C#, para alegría de muchos.

A grandes rasgos, esta característica nos permite especificar valores por defecto para los parámetros de nuestros métodos, ahorrándonos tiempo de codificación:

class Program
{
    public static void Main(string[] args)
    {
        Muestra();            // Imprime 1,1
        Muestra(8);           // Imprime 8,1
        Muestra(3,4);         // Imprime 3,4
        Console.ReadKey();
    }
 
    static void Muestra(int a=1, int b=1)
    {
        Console.WriteLine(a + "," + b);
    }
}

Desde siempre, ignorante de mí, había pensado que esto no era más que una triquiñuela del compilador, un azucarillo sintáctico destinado a facilitarnos la creación de sobrecargas de forma rápida, pero, ahora que lo he estudiado algo más a fondo, resulta que no es así. De hecho, los parámetros opcionales están soportados a nivel de plataforma, y funcionan de forma algo extraña (o al menos diferente a lo que podía esperarse), por lo que es conveniente conocerlos bien para no cometer determinados errores.

En primer lugar, me ha llamado la atención que la detección de la ausencia de parámetros en la llamada y la asignación de los valores por defecto no la realiza el método en el que se han definido. Es decir, sobre el ejemplo anterior, no es el método Muestra() el que comprueba si se han suministrado valores para los parámetros a y b, ni el que le asigna los valores por defecto en caso contrario. Esta “comprobación” se realiza en tiempo de compilación (!).

Esto lo demostramos muy fácilmente si descompilamos esta misma aplicación con ayuda del imprescindible Reflector, que nos mostrará el siguiente código:

class Program
{
    public static void Main(string[] args)
    {
        Muestra(1, 1);
        Muestra(8, 1);
        Muestra(3, 4);
        Console.ReadKey();
    }
 
    public static void Muestra([Optional, DefaultParameterValue(1)] int a, 
     [Optional, DefaultParameterValue(1)] int b)
    {
        Console.WriteLine(a + ", " + b);
    }
}

Como se puede observar, se ha generado un método Muestra() cuyos parámetros incluyen atributos que indican su opcionalidad y el valor por defecto en cada caso.

Pero lo curioso está en el método Main(), desde donde se hacen las llamadas, el que podemos observar que las invocaciones a Muestra() incluyen valores para todos los parámetros, como si se tratara de constantes especificadas directamente por el desarrollador.

Por tanto, no hay nada mágico en los métodos con parámetros opcionales, ni sobrecargas, ni código de comprobación o asignación insertado de forma automática. Es el propio compilador el que, en el momento de generar el código IL, extrae los valores por defecto de los parámetros no especificados en la llamada examinando los atributos presentes en la signatura y los introduce en la invocación.

Y es aquí justo donde hay que tener cuidado al utilizar los parámetros opcionales. Dado que el valor de los parámetros se determina en tiempo de compilación y se incluyen como constantes en el código IL generado, pueden producirse efectos no deseados si trabajamos con distintos ensamblados.

Veámoslo con un ejemplo, que, aunque poco real, creo que ilustrará un escenario donde los parámetros opcionales podrían jugarnos una mala pasada.

En el siguiente código, perteneciente al ensamblado LogicaNegocio.dll, vemos un método CalculaImporteconIva(), que retorna un importe aplicándole el impuesto (IVA) correspondiente:

public class LogicaNegocio
{
    public double CalculaImporteConIva(double importe, double iva = 0.16)
    {
        return importe + importe*iva;
    }
}

Así, podemos tener un ensamblado externo, pongamos ERP.exe, que haga uso de este método de la siguiente forma:

public void muestraDesglose(double importe)
{
    double importeTotal = logica.CalculaImporteConIVA(importe)
    // Mostrarlo...
}

En el momento de compilación de ERP.exe, la llamada anterior quedará en el ensamblado resultante igual que si hubiéramos hecho esta llamada:

 
    double importeTotal = logica.CalculaImporteConIVA(importe, 0.16)

Si ahora se produce una subida de IVA (como lamentablemente va a ocurrir en breve), acudiríamos a modificar el valor por defecto del parámetro iva en el método CalculaImporteConIva() y recompilaríamos LogicaNegocio.dll:

public class LogicaNegocio
{
    public double CalculaImporteConIva(double importe, double iva = 0.18)
    {
        return importe + importe*iva;
    }
}

Sin embargo, si no recompilamos ERP.EXE desde éste seguiríamos enviándole el valor anterior (0.16, recordad que este valor aparece como constante en el ensamblado), lo que podía provocar algún problema. Es decir, si queremos mantener la coherencia del sistema, nos veríamos obligados a recompilar todos los ensamblados que referencien LogicaNegocio.dll.

Conclusión: en métodos públicos, y especialmente en aquellos que serán consumidos desde ensamblados externos, es conveniente utilizar parámetros opcionales sólo cuando los valores constantes sean “verdades universales”, como las constantes matemáticas o datos que con toda seguridad no van a cambiar.  No es buena idea utilizarlos para reflejar valores variables o constantes de lógica de negocio, con posibilidades de cambio aunque sean remotas.

Por último, comentar que aunque este post está centrado en C#, todas estas precauciones son igualmente válidas para Visual Basic .NET.

Publicado en: Variable not found

¿Esa enumeración está vacía?

¿Sómos los primeros en llegar al estadio? Casualmente encuentro en el post de Chris Eargle “Any() versus Count()” un tema del que pensaba escribir hace tiempo y al final dejé en el tintero: ¿cómo podemos determinar si una enumeración está vacía? Vale, es bien fácil, una enumeración está vacía si tiene cero elementos.

Si trabajamos con un array, podemos consultar la propiedad Length; si se trata de una colección, podemos utilizar la propiedad Count, que también nos devolverá el mismo dato de forma directa.

Sin embargo, cuando procesamos información es frecuente tratar con tipos de datos enumerables cuya implementación exacta desconocemos, y en los que no tenemos acceso a ninguna propiedad que nos permita conocer el número de elementos exactos que tiene almacenados. Por ejemplo, esto ocurre cuando obtenemos un conjunto de datos en forma de IEnumerable o trabajamos con LINQ sobre alguna fuente de información.

Pues bien, en estos casos hay que ser algo prudentes con la forma de consultar el número de elementos. Me he topado con código propio en el que, simplemente por despiste, utilizaba el método Count(), facilitado por LINQ para las enumeraciones y otras colecciones de datos, con objeto de saber si una lista estaba vacía:

var prods = services.GetProductsByCategory(category);
if (prods.Count() > 0)
{
   // Hacer algo con los elementos de la lista
}
else
{
   // No hay elementos
}

Seguro que ya os habréis percatado de que eso está mal, muy mal. El método Count() recorrerá uno por uno los elementos para contarlos, retornando el número exacto de ellos. En escenarios de un gran número de elementos, o cuando es costoso obtener cada elemento (como al traerlos de una base de datos) puede suponer un consumo de recursos enorme.

Y justo por esto existe el método Any(), que comprueba únicamente si existe al menos un elemento en la colección. Internamente este método itera sobre la colección retornando true cuando encuentra el primer elemento, por lo que es mucho más eficiente que el anterior:

var prods = services.GetProductsByCategory(category);
if (prods.Any()) // Mucho mejor :-)
{
  // Hacer algo con los elementos de la lista
}
else
{
  // No hay elementos
}

La utilización de Any() también es interesante para comprobar la existencia de elementos que cumplan una condición, expresada en forma de predicado; retornará true cuando encuentre el primer elemento que lo haga cierto:

if (pedidos.Any(p => p.Pendiente))
{
   // Mostrar alerta, hay al menos un pedido pendiente

}

Estamos en la puerta de un Estadio y queremos saber si vamos a ser los primeros en entrar al recinto… ¿le preguntaríamos al portero el número exacto de aficionados que ya están dentro para, si es cero, saber que somos los primeros? ¿O nos bastaría simplemente con preguntarle si hay alguien? ;-P

Publicado en: Variable not found
Hey: ¡estoy en Twitter!

NDepend 3, radiografía tu código aún más fácilmente

Hace casi un año hablaba de la segunda versión de NDepend, una herramienta capaz de ayudaros a mejorar nuestro código, analizando cientos de aspectos, métricas y reglas a nivel de fuentes y ensamblados.

Recientemente se ha publicado la tercera versión de NDepend, que ofrece interesantes novedades respecto a las anteriores, como la integración absoluta con Visual Studio, el soporte para soluciones multi-proyecto, potentes mecanismos de búsqueda, edición múltiple de CQL, o el seguimiento de cambios, además de las tradicionales características del producto.

Los 10 métodos más extensos de ASP.NET MVC 2

Pero sin duda, lo que me ha parecido más espectacular es su magnífica integración con Visual Studio (2005, 2008 y 2010), que permite disfrutar de toda la potencia de análisis de NDepend, y sus facilidades para la navegación desde el mismo entorno de desarrollo.

Para habilitar esta característica es necesario instalar el plugin en el IDE, que se realiza desde el propio entorno visual de NDepend:

Instalar plugin NDepend en Visual Studio

De esta forma, ya no es necesario acudir a la herramienta Visual NDepend para realizar búsquedas, comprobar reglas o navegar a través de la base de código: lo haremos directamente desde VS, utilizando los menús contextuales. Y gracias a ello, podemos disfrutar de las nuevas opciones de navegación, que nos permitirá surcar el código utilizando rutas distintas a las habituales:

Navegar por el código con NDepend

U obtener diagramas de dependencias de componentes, utilizando el botón derecho del ratón:

Dependencias con NDepend 3

Además, como comenta Patrick Smacchia, padre de la criatura, el rendimiento del entorno prácticamente no se resiente, dado que los análisis se ejecutan en segundo plano de forma incremental.

Recordar, por último, que NDepend es una aplicación comercial, pero dispone de una versión limitada gratuita utilizable por universidades, desarrolladores open source e incluso, durante un tiempo determinado, de prueba en proyectos comerciales.

Página del producto: http://www.ndepend.com/
Radiografiado desde: NDepend 3, radiografía tu código aún más fácilmente @ Variable not found
Hey, ¡estoy en twitter!

Forzar validación desde cliente con ASP.NET Webforms

 

Validación de formularios ASP.NET desde javascript Cada vez que tengo que forzar la validación de los datos de un formulario Webforms mediante javascript me veo obligado a preguntarle a Google, ese que todo lo sabe, cómo era el nombre de la función. Cosas de la edad, supongo 😉

Así que, a modo de auto-recordatorio y con la intención de que pueda ser útil a alguien más, ahí va: la función se llama Page_ClientValidate(). Retorna “true” si, una vez evaluados todos los validadores de la página, el valor de los campos es correcto (al menos en cliente; otra cosa son las comprobaciones en servidor, p.e., las definidas en un CustomValidator).

Y como ejemplo de uso, puede valer el siguiente. Se trata de un botón de envío en un formulario donde se compone un correo electrónico:

   1: ...

   2: <asp:Button ID="btnEnviar" runat="server" Text="Enviar mail" 

   3:      OnClick="btnEnviar_Click"

   4:      OnClientClick="return confirmar();"

   5: />

   6: ...

   7:  

   8: <script type="text/javascript">

   9:     function confirmar() {

  10:         if (!Page_ClientValidate())   // Fuerza la validación en cliente

  11:             return false;             

  12:             

  13:         return confirm('¿Seguro que desea realizar el envío?');

  14:     }

  15: </script>

 

Como se puede observar, en el atributo OnClientClick del botón incluye un script en el que se retorna el valor devuelto por la función confirmar. Si el retorno es false, se cancela el Postback, evitando así que se invoque al evento btnEnviar_Click que es el que realiza el envío del mail propiamente dicho.

En el cuerpo de la función confirmar(), en primer lugar, validamos la página; si ésta no supera el proceso, los validadores habrán mostrado sus mensajes de error y retornamos falso, haciendo que se anule el postback. Si la validación es correcta, solicitamos al usuario que confirme la acción y retornamos su decisión.

Publicado en: Variable not found.

Fluent NHibernate 1.0 publicado

A finales del agosto, James Gregory anunció la publicación de la versión 1.0 de Fluent NHibernate, una librería que ofrece una ágil alternativa a los espesos archivos de configuración de NHibernate.

Su API permite configurar desde el código de una aplicación, de forma fluida la mayoría de las veces, los mapeos entre la estructura de una base de datos relacional y el modelo de objetos que utiliza. Así, evitaremos la manipulación de grandes archivos XML, a la vez que podremos beneficiarnos de la validación en tiempo de compilación y, por supuesto, de posibilidades como la refactorización y el intellisense durante el desarrollo.

El siguiente código muestra el mapeo de la entidad Cat con sus propiedades, algunas de ellas con restricciones, y relaciones con otras entidades a uno (References) y a varios (HasMany); el nombre de la tabla y campos en el modelo relacional es el mismo que el de las propiedades, gracias a la convención sobre configuración, lo que permite simplificar código respecto a su equivalente XML:

public class CatMap : ClassMap<Cat>

{

  public CatMap()

  {

    Id(x => x.Id);

    Map(x => x.Name)

      .Length(16)

      .Not.Nullable();

    Map(x => x.Sex);

    References(x => x.Mate);

    HasMany(x => x.Kittens);

  }

}

Como podemos observar, el equivalente XML es mucho más verboso:

<?xml version="1.0" encoding="utf-8" ?>  

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"  

  namespace="QuickStart" assembly="QuickStart">  

 

  <class name="Cat" table="Cat">  

    <id name="Id">  

      <generator class="identity" />  

    </id>  

 

    <property name="Name">  

      <column name="Name" length="16" not-null="true" />  

    </property>  

    <property name="Sex" />  

    <many-to-one name="Mate" />  

    <bag name="Kittens">  

      <key column="mother_id" />  

        <one-to-many class="Cat" />  

      </bag>  

  </class>  

</hibernate-mapping>

Otra de las ventajas que aporta es el auto-mapping, que hace utilización intensiva del principio de convención sobre configuración para generar de forma automática mapeos de aquellas entidades que atiendan a unas normas preestablecidas (aunque modificables). El siguiente código muestra la forma tan sencilla de crear los mapeos de todas las entidades definidas en el espacio de nombres MiApp.Entidades, dentro del ensamblado donde se definió la entidad Producto:

var autoMappings = AutoMap.AssemblyOf<Producto>()

  .Where(t => t.Namespace == "MiApp.Entidades");

Además del mapeo objeto-relacional, el software abarca también la configuración del acceso a datos de NHibernate a través de su interfaz de programación. El siguiente código muestra la inicialización de la conexión a una base de datos SQL Server 2005, tomando la cadena de conexión del AppSettings y mapeando automáticamente las entidades que se encuentren definidas en un namespace concreto:
 
var sessionFactory = Fluently.Configure()

    .Database(MsSqlConfiguration.MsSql2005

      .ConnectionString(c => c.FromAppSetting("connectionString"))

    .Mappings(m => m.AutoMappings.Add(

      AutoMap.AssemblyOf<Producto>(type => type.Namspace.EndsWith("Entidades"))))

  .BuildSessionFactory();

Y por último, hay otra característica muy interesante vistas a la realización de pruebas unitarias sobre los mecanismos de persistencia. El código mostrado a continuación crea una instancia de la clase Empleado, la inserta en la base de datos, realiza una lectura de la entidad y la compara con la original, de forma automática:

[Test]

public void EmpleadoMapeaCorrectamente()

{

    new PersistenceSpecification<Empleado>(session)

        .CheckProperty(emp => emp.Id, 1)

        .CheckProperty(emp => emp.Nombre, "José")

        .CheckProperty(emp => emp.Apellidos, "Aguilar")

        .VerifyTheMappings();

}

Más información en la wiki del proyecto. Y las descargas, desde aquí.

Publicado en: Variable not found.

Inicializar a null propiedades sin tipo

No es algo que ocurra muy frecuentemente, pero en determinadas ocasiones puede ser útil inicializar una propiedad de un tipo anónimo con el valor nulo, por ejemplo:

var conductor = new { Nombre = ”Marisa”, Edad = 34, Auto = “Renault Megane” };
var peaton = new { Nombre = “Juan”, Edad = 43, Auto = null };

En el código anterior se entiende que lo que queremos indicar estableciendo la propiedad Auto a null es que la persona que estamos representando no dispone de automóvil, o bien no conocemos este dato.

Algo muy normal… sino fuera porque el compilador se empeña en abofetearnos con el siguiente error:

Cannot assign <null> to anonymous type property

Y la verdad, en cuanto lo pensamos un poco, tiene sentido. En los casos anteriores, se está instanciando un tipo anónimo cuyas propiedades son generadas en tiempo de compilación. En el caso anterior, el compilador crearía para el tipo anónimo una propiedad “Nombre” de tipo string, una “Edad” de tipo integer, y una “Auto”, de tipo… ¿qué tipo podría asignar el compilador a esta propiedad? Ese es precisamente el problema.

Lo mismo ocurre al intentar inicializar variables implícitamente tipadas con el valor nulo; aunque cambia el error, el concepto idéntico, el compilador no es capaz de inferir el tipo de la variable:

var persona = null; // Error: Cannot assign <null> to an implicitly-typed local variable

La forma de solucionarlo en ambos casos es muy sencilla: basta con informar explícitamente al compilador del tipo de la propiedad, para que pueda crearla sin problema, por ejemplo así:

var peaton = new { Nombre = "Juan", Edad = 43, Auto = (string)null };

var persona = null as Persona;

Publicado en: Variable not found

Linq para NHibernate, versión 1.0

Logo de NHibernate Hace unas semanas, Oren Eini (o Ayende Raihen, como se le suele conocer) comunicaba la liberación de la versión 1.0 del proveedor de Linq para NHibernate, una característica altamente demandada por los usuarios desde la aparición del lenguaje de consulta integrado en .NET.

Aunque será incluido como parte del producto NHibernate en versiones venideras, han decidido liberar la actual release del proveedor como paquete independiente para que pueda comenzar a utilizarse desde este momento. Está siendo testado en multitud de aplicaciones en producción desde hace varios años, y al parecer el funcionamiento es más que correcto.

¿Y cómo puede ayudarte este proveedor, si eres usuario de NHibernate? El siguiente ejemplo, tomado de Caffeinated Coder muestra cómo una consulta a base de datos puede simplificarse y hacerse mucho más legible utilizando Linq, además de beneficiarse del tipado fuerte, intellisense y comprobaciones en tiempo de compilación:

Utilizando el API de NHibernate:

public IList<Call> GetCallsByDate(DateTime beginDate, int interpreterId)   

{   

    ICriteria criteria = Session.CreateCriteria(typeof(Call))

        .CreateAlias("Customer", "Customer")

        .Add(Restrictions.Gt("StartTime", beginDate))

        .Add(

            Restrictions.Or(

                Restrictions.Lt("EndTime", DateTime.Now), Restrictions.IsNull("EndTime"))

            )

        .Add(Restrictions.Eq("Interpreter.Id", interpreterId))

        .AddOrder(Order.Desc("StartTime"))

        .AddOrder(Order.Desc("Customer.Name")); 

        return criteria.List<Call>() as List<Call>;

}

Utilizando Linq:

public IList<Call> GetCallsByDateWithLinq(DateTime beginDate, int interpreterId)  

{   

    var query = from call in Session.Linq<Call>()

        where call.StartTime > beginDate

            && (call.EndTime == null || call.EndTime < DateTime.Now )   

            && call.Interpreter.Id == interpreterId

        orderby call.StartTime descending, call.Customer.Name

        select call;

 

    return query.ToList();

}

Podéis descargar tanto los binarios como el código fuente desde la página del proyecto en SourceForge.

Publicado en: Variable not found.