Outlook 2010. El tamaño de los datos adjuntos excede el límite permitido

Sé que no es un post de programación, pero me ha tenido un rato entretenido y he considerado oportuno comentarlo. Así, por un lado, si alguien puede aprovecharlo, bien y si no, seguro que me sirve de recordatorio cuando tenga que volver a utilizarlo.

El caso es que necesitaba enviar un fichero por correo electrónico que ocupaba la friolera de 30 Mb. Hasta el momento, tanto mi servidor de correo como el de la persona a la que se lo enviaba aceptan tales tamaños pero esta vez la limitación no ha venido desde el servidor. La restricción me la ha puesto directamente el Outlook que utilizo (está en el portátil, así que nada de Exchange Server). Al intentar adjuntar el archivo me encuentro el siguiente mensaje de error.

image

¡Qué contentos nos solemos poner cuando se incluyen en las aplicaciones mejoras que nos facilitan la vida y qué poca gracia nos hace cuando estos cambios alteran nuestras costumbres!.

Hace poco, de un artículo de José M. Aguilar, aprendí el término procastinación y decidí predicar con el ejemplo, así que, en lugar de empezar a fragmentar el archivo y enviar varios mensajes, me puse a investigar un poco sobre el tema. La verdad es que no tardé en llegar a la solución en la página de Microsoft: http://support.microsoft.com/kb/2222370.

Para aquellos que no quieran leer la solución desde su ubicación original, me limito a resumirla:

Outlook, en su versión 2007 no permite el envío de adjuntos de más de 20Mb, así que, para condicionar esta situación, bastará con acudir al registro del sistema, el que deberemos localizar cualquiera de las claves siguientes:

  • HKEY_CURRENT_USERSoftwareMicrosoftOffice14.0OutlookPreferences
  • HKEY_CURRENT_USERSoftwarePoliciesMicrosoftOffice14.0OutlookPreferences

Deberemos añadir la siguiente subclave:

  • Tipo de valor: Valor DWORD
  • Nombre: MaximumAttachmentSize
  • Valor: Podemos especificar un valor en Kb que será el máximo permitido o 0 en caso de que no queramos que Outlook imponga ninguna restricción.

Una vez reiniciado el Outlook ya podremos enviar cualquier tamaño de adjuntos, siempre y cuando nos lo permitan los servidores de correo.

Claro, que ya que escribimos en un blog en el que prima el desarrollo no vamos a dejar de incluir alguna rutina Risa, así que, si queremos hacer lo mismo desde C#, pues dejo el código:

 

using Microsoft.Win32;

namespace JnSoftware.Office
{
    /// <summary>
    /// Utilidades de Outlook
    /// </summary>
    class Outlook
    {
        /// <summary>
        /// Elimina la limitación por tamaño de enviar documentos adjuntos
        /// </summary>
        public void EliminaLongitudAdjuntos()
        {
            EstableceLongitudAdjuntos(0);
        }


        /// <summary>
        /// Establece el tamaño máximo de ficheros adjuntos
        /// </summary>
        /// <param name="MbSizeMaximum">Valor que queremos 
        /// establecer como máximo para el tamaño de los ficheros
        /// adjuntos</param>
        public void EstableceLongitudAdjuntos(int MbSizeMaximum)
        {
            RegistryKey kRoot = Registry.CurrentUser;
            string rama = 
                  "Software\Microsoft\Office\14.0\Outlook\Preferences";

            RegistryKey rk = kRoot.OpenSubKey(rama,true );
            rk.SetValue("MaximumAttachmentSize", MbSizeMaximum * 1024);
        }
    }
}

Codificación Unicode

Ya que tengo alguna que otra primavera a cuestas y que suelo ganarme la vida, entre otras cosas, impartiendo clases, reconozco que tengo alguna que otra manía. Una de estas manías está estrechamente ligada con la ortografía (u hortografía, como vi no hace mucho en el escrito de un alumno).

Hace poco, en la creación de una página web, el cliente solicitó uno de aquellos enlaces que, al hacer un clic sobre él, se abriera el cliente de correo. Para la mayor parte de clientes, sólo existe el Outlook y, sinceramente, creo que es mejor porque hace muy poquito, el jefe de una empresa en la que trabajo me comentó que quería usar el OpenOffice y, claro, iba a instalar el “gestor de correos del OpenOffice, es decir, el Ubuntu” –casi me da algo- Lástima que no sé callar, puesto que se hubiera merecido instalar este “cliente de correo” sobre el Windows XP, más que nada, por la supremacía que intentaba transmitir al hablarme del tema…

Bueno, siempre suelo irme por las ramas, así que retomo el tema inicial. El caso es que quería añadir, en una página HTML un enlace para que se abriese el Outlook del visitante al site, así que, como una vez hice un cursillo de HTML, me pongo a teclear:

<HTML>
 <HEAD>
  <TITLE> Mail Sample </TITLE>
 </HEAD>
 <BODY>
  <a
    href="mailto:usuario@dominio.com
      ?subject=Solicitud de información
      &body=Les agradecería se sirvan remitirme información adicional acerca de:
      ">
      Texto del enlace
</a>
 </BODY>
</HTML>

A partir de aquí, haces las pruebas y todo va bien, puesto que yo también suelo usar IE y Outlook. El problema empieza a sospecharse cuando recibes correos con caracteres extraños y empiezas a probar con algún explorador diferente, como Firefox. El resultado viene a ser el siguiente:

image

¡Cáspita! Los acentos no acaban de verse como yo esperaba.

La verdad es que no sé si existe alguna solución sencilla para arreglar esto, pero entre que una vez estudié algo de codificación en la asignatura “Estructura de computadores” y que soy retorcido por naturaleza, hecho mano de VisualStudio y me pongo a intentar encontrar una solución.

Tras estar un rato buscando en las clases de .Net Framework, veo que existe una clase dedicada a la conversión entre codificaciones de caracteres, la clase System.Text.Encoding, así que no resulta difícil crear una función que convierta una serie de caracteres escritos en ASCII a Unicode:

using System;
using System.Text;

namespace JnSoftware.Utiles.CodificacionUnicode
{
    /// <summary>
    /// Servicios de codificación Unicode
    /// </summary>
    class CodificacionUnicode
    {
        /// <summary>
        /// Devuelve la cadena pasada como parámetro en formato 
        /// Unicode
        /// </summary>
        public static string CodificaUnicode(string texto)
        {
            /* Tipo de codificación */
            System.Text.Encoding codificacion = System.Text.Encoding.Unicode;

            /* Se crea una matriz de CHAR con los carácteres de la cadena que 
             * se quiere codificar */
            Char[] caracteres = texto.ToCharArray();

            /* Conversión de la cadena en una matriz de Bytes */
            Byte[] bytesCodificados = codificacion.GetBytes(caracteres);

            /* Se crea un StringBuilder para poder traspasar los bytes ya 
             * codificados */
            StringBuilder sb = new StringBuilder();

            /* Bucle que añade los bytes codificados. Se puede aprovechar para 
             * poder comprobar caracteres especiales*/
            for (int c = 0; c < bytesCodificados.Length - 1; c++)
            {
                string byteActual = string.Format("%{0:X2}", bytesCodificados[c]);

                /* Se comprueba que no se trate del carácter "especial" del separador */
                if (!byteActual.Equals("%00"))
                    sb.Append(byteActual);
            }
            /* Valor de retorno */
            return sb.ToString();
        }
    }
}

Por cierto, una vez aplicada la función, el enlace queda un poco más “sucio”, pero mucho más compatible:

<a
  href="mailto:
    %75%73%75%61%72%69%6F%40%64%6F%6D%69%6E%69%6F%2E%63%6F%6D
    ?subject=%53%6F%6C%69%63%69%74%75%64%20%64%65%20%69%6E%66%6F%72%6D%61%63%69%F3%6E
    &body=%4C%65%73%20%61%67%72%61%64%65%63%65%72%ED%61%20%73%65%20%73%69%72%76%61%6E
          %20%72%65%6D%69%74%69%72%6D%65%20%69%6E%66%6F%72%6D%61%63%69%F3%6E%20%61%64
          %69%63%69%6F%6E%61%6C%20%61%63%65%72%63%61%20%64%65%3A
    ">
    Texto del enlace
  </a>

 

Nota: Reconozco que tenía esta entrada en el viejo blog, pero como tengo intención de cerrarlo definitivamente, intento “salvar” aquellas cosas que he ido necesitando después de escribirlas. En este caso, tan sólo me he limitado a traducirlo de C#.

C#: Nombre de fichero aleatorio

Cuando escribes un post y tienes la responsabilidad de hacerlo en un lugar como puede ser geeks.ms, intentas que tenga un mínimo de calidad. En este caso sé que no es así, aunque no puedo evitar hacerlo, puesto que es un tema que me ha tocado las narices más de una vez.

En más de una ocasión me he encontrado con la necesidad de tener que tocar un fichero temporal, tanto en WinForms como en WebForms para, por ejemplo, descomprimir un fichero, tratar un documento en la web para descargarlo posteriormente… El caso es que, en un momento dado, necesitamos tener un nombre de fichero “aleatorio” para poder tratarlo de manera temporal. Nada más lejos de la realidad o, mejor dicho, nada más lejos de las librerías de .Net Framework, puesto que el propio Framework se encarga de, como casi siempre, solventarnos el problema…

El método GetRandomFileName() de la clase System.IO.Path nos permitirá tener un nombre de fichero aleatorio, el cual podremos usar sin tener que hacer uso de librerías propias extrañas para poder crear un fichero en una carpeta determinada.

Lo sé y pido disculpas por ello… No es un post con una calidad aceptable, pero creo que, de paso que puede servirme de recordatorio a mí, igual también tiene utilidad para aquellos a los que la memoria les falla tanto como a mí… ejemmm

string fileRandom = System.IO.Path.GetRandomFileName();

— Editado el día 28/2/11 para comentar lo siguiente:

En base a los comentarios, ¿qué se me ocurrió hacer?. Pues casi nada… Tan sólo dejar el ordenador almacenando nombres de ficheros generados por la función GetRandomFileName en una base de datos. Como no tenía mucha prisa, miraba, antes de la inserción a ver si existía. Resultado:

Dos millones de registros y no ha encontrado ningún duplicado. No es que la estadística sea mi fuerte, así que teniendo en cuenta que los nombres de los ficheros son de 8 caracteres más una extensión de 3,conteniendo letras y número, es fácil deducir que existen muchas más posibilidades que la cifra de dos millones, pero bueno, yo creo que ya ha llegado el momento de dejar de jugar con esto e intentar dedicarme a algo más serio… jeje

C#: Calcular Semana Santa

Siempre he pensado que todo programador, de vez en cuando necesitamos alguna ida de olla y, sencillamente, nos sentamos delante del ordenador más por entretenimiento que por necesidad. En mi caso por lo menos, cuando ando muy agobiado con el resto del entorno, es algo que me libera bastante. Claro, que suele liberarme mucho más salir a tomar unas cuantas cervezas con los amiguetes, pero como en este caso no tenía ganas de estar resacoso a la mañana siguiente, pues opté por hacer algo que me entretuviese un rato y que fuera más sano.

Uno de los últimos artículos que publiqué en mi blog anterior versaba sobre el cálculo de ciertos días relacionados con la Semana Santa. No es que el día que lo hice tuviese también una pájara, es que ya se me van acabando las ideas para los ejercicios que tengo que ir haciendo para los alumnos (y eso que el ejercicio era de Excel).

Bueno, nos dejaremos de charlas superfluas como si estuviésemos en la barra del bar y, ya que lo hice, lo dejo para todo aquel que quiera aprovecharlo o, sencillamente, quiera distraerse un rato como yo. El caso es que me entretuve en crear un calendario en el que se pueden ver resaltados los días de Semana Santa de los años comprendidos en el intervalo (1900,2100). He dejado una versión del calendario en la página http://velasco.biz/html/SemanaSanta/Default.aspx, no para hacer publicidad, puesto que es una página que actualizo casi cada año (quizás dos) sino para poder ver el ejemplo funcionando.

Para la creación del calendario, una página con un combo para poder seleccionar el año (Vale, sé que es un DropDownList, pero no sé dónde habré oído lo del Combo) y una tabla de servidor sin ninguna celda es suficiente (ya la rellenamos dinámicamente)

 

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" 
    Inherits="JnSoftware.Samples.WebCalendar.Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Semana Santa - Sample</title>
</head>
<body>
    <form 
            id="form1" 
            runat="server">
    <h1>Semana Santa</h1>
    <br />
    Selecciona el año:
    <asp:DropDownList 
            ID="lstAnios" 
            runat="server" 
            Width="100px" 
            AutoPostBack="True"
            OnSelectedIndexChanged="lstAnios_SelectedIndexChanged">
    </asp:DropDownList>
    <div>
        <asp:Table 
            runat="server" 
            ID="tbContenido">
        </asp:Table>
    </div>
    </form>
</body>
</html>

 

El code-behind de la página tampoco es que sea excesivamente complicado. Lo que hago es que, una vez cargado los años que sé calcular en el combo (método CargaComboAnios), tan sólo es cuestión de crear un objeto Calendar para cada uno de los meses del año (método CargaCalendarios).

Quizás, el método que más me costó, imagino que por no estar lo suficientemente despierto es el encargado de marcar los días que quería (CargaDiasSemanaSanta), puesto que me empeciné en hacer cosas raras al ver que los días de Semana Santa podían llegar a dividirse en dos meses diferentes (no pongo nada de lo que intenté porque realmente me avergüenza). Al final opté por la creación de un Array de fechas y un recorrido por el mismo, de esta manera, incluso, puede aprovecharse para añadir otras fechas.

 

using System;
using System.Web.UI.WebControls;
using JnSoftware.Calculos;

namespace JnSoftware.Samples.WebCalendar
{
    public partial class Default : System.Web.UI.Page
    {
        // Personalización del número de filas y columnas de la tabla.
        int columnas = 3;
        int filas = 4;

        #region Eventos del WebForm
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                cargaComboAnios();
                cargaCalendarios();
            }
        }

        protected void lstAnios_SelectedIndexChanged(object sender, EventArgs e)
        {
            DropDownList combo = (DropDownList)sender;
            if (combo.SelectedValue != null)
                cargaCalendarios();
        }
        #endregion

        /// <summary>
        /// Realiza la carga de los años en el combo.
        /// </summary>
        private void cargaComboAnios()
        {
            DropDownList combo = lstAnios;
            for (int i = 1900; i <= 2100; i++)
                combo.Items.Add(
                    new ListItem(
                        text: i.ToString("#,#"),
                        value: i.ToString()
                    )
                );
            // Selección del año actual
            combo.SelectedValue = DateTime.Today.Year.ToString();
        }

        /// <summary>
        /// Realiza la carga de los calendarios en la tabla.
        /// </summary>
        private void cargaCalendarios()
        {
            Table tabla = tbContenido;
            tabla.Rows.Clear();

            // Formato de la tabla
            tabla.CellSpacing = 5;

            // Bucle de filas
            int mes = 1;
            for (int f = 0; f < filas; f++)
            {
                TableRow tr = new TableRow();

                // Bucle de celdas
                for (int c = 0; c < columnas; c++)
                {
                    TableCell tc = new TableCell();
                    tc.Controls.Add(GetCalendar(mes++));
                    tr.Cells.Add(tc);
                }
                tabla.Rows.Add(tr);
            }

            /* Cargados los calendarios, se marcan los días de 
             * semana santa */
            cargaDiasSemanaSanta();
        }

        /// <summary>
        /// Obtiene un objeto Calendar del mes pasado como parámetro.
        /// Este objeto Calendar se agregará a la tabla.
        /// </summary>
        private Calendar GetCalendar(int mes)
        {
            int anio = int.Parse(lstAnios.SelectedValue);

            Calendar c = new Calendar();
            // Id del control
            c.ID = "mes" + mes.ToString("00");
            // Selección del mes
            c.VisibleDate = new DateTime(anio, mes, 1);
            // Se desactiva la navegación
            c.ShowNextPrevMonth = false;
            // Sólo se muestra el mes en el encabezado.
            c.TitleFormat = TitleFormat.Month;
            // Desactivamos la selección
            c.SelectionMode = CalendarSelectionMode.None;
            // Pongamos la firma
            c.ToolTip = "JnSoftware - Calendario de "
                    + c.VisibleDate.ToString("MMM/yy");
            return c;
        }

        /// <summary>
        /// Marca los días de semana santa en el calendario
        /// </summary>
        private void cargaDiasSemanaSanta()
        {
            int anio = int.Parse(lstAnios.SelectedValue);

            SemanaSanta s = new SemanaSanta(anio);
            DateTime JuevesSanto = s.JuevesSanto;

            /* Tomamos como fiestas de semana Santa desde el 
             * jueves hasta el lunes */
            DateTime[] fechasSemanaSanta = new DateTime[5];
            for (int i = 0; i < 5; i++)
                fechasSemanaSanta[i] = JuevesSanto.AddDays(i);

            /* Se marcan los días en los calendarios */
            Table tabla = tbContenido;
            foreach (DateTime dia in fechasSemanaSanta)
            {
                int mes = dia.Month - 1;
                int fila = mes / columnas;
                int columna = mes % columnas;

                Calendar calendar = 
                    tabla.Rows[fila].Cells[columna].Controls[0] as Calendar;
                calendar.SelectedDates.Add(dia);
            }
        }
    }
}
 
 
Al final, este ha sido el resultado. Lo que no llegué a conseguir es la posibilidad de incluir algún tipo de ToolTip a las fechas añadidas para poder insertar el día de la fiesta o fecha señalada (si algún alma caritativa ha logrado algo similar, siempre sería de agradecer cualquier comentario).
Bueno, hasta aquí el post actual. Incluyo también la clase que ya publiqué en su día para el cálculo de los días de la Semana Santa, así queda todo como más ordenado.
 
using System;

namespace JnSoftware.Calculos
{

    /// <summary>
    /// Cálculo de Semana Santa
    /// </summary>
    public class SemanaSanta
    {

        #region Campos 
        private int a;
        private int b;
        private int c;
        private int d;
        private int e;
        private DateTime pascuaResurreccion;

        private int anio;

        #endregion

        /// <summary>
        /// Constructor de la clase
        /// </summary>
        /// <param name="anio">Entero que representa el año del que 
        /// se quiere calcular la semana santa.</param>
        /// <exception cref="ArgumentOutOfRangeException">
        /// Se produce cuando se intenta calcular
        /// la semana santa de un año no contemplado.</exception>
        public SemanaSanta(int anio)
        {
            try
            {
                this.anio = anio;
                calculaDomingoPascua();
            }
            catch { throw; }
        }

        /// <summary>
        /// Cálculo del domingo de Pascua o domingo de Resurrección.
        /// </summary>
        private void calculaDomingoPascua()
        {
            ParConstantes p = getPar(anio);
            a = anio % 19;
            b = anio % 4;
            c = anio % 7;
            d = (19 * a + p.M) % 30;
            e = (2 * b + 4 * c + 6 * d + p.N) % 7;

            if (d + e < 10)
                pascuaResurreccion = new DateTime(anio, 3, d + e + 22);
            else
                pascuaResurreccion = new DateTime(anio, 4, d + e - 9);

            // Excepciones
            if (pascuaResurreccion == new DateTime(anio, 4, 26))
                pascuaResurreccion = new DateTime(anio, 4, 19);

            if (pascuaResurreccion == new DateTime(anio, 4, 25) 
                    && d == 28 && e == 6 && a > 10)
                pascuaResurreccion = new DateTime(anio, 4, 18);
        }

        #region Constantes cálculo
        private struct ParConstantes
        {
            public int M { get; set; }
            public int N { get; set; }
        }

        private ParConstantes getPar(int anio)
        {
            ParConstantes p = new ParConstantes();
            if (anio < 1583)
            {
                throw
                    new ArgumentOutOfRangeException
                        ("El año deberá ser superior a 1583");
            }
            else if (anio < 1700) { p.M = 22; p.N = 2; }
            else if (anio < 1800) { p.M = 23; p.N = 3; }
            else if (anio < 1900) { p.M = 23; p.N = 4; }
            else if (anio < 2100) { p.M = 24; p.N = 5; }
            else if (anio < 2200) { p.M = 24; p.N = 6; }
            else if (anio < 2299) { p.M = 25; p.N = 0; }
            else
            {
                throw
                    new ArgumentOutOfRangeException
                        ("El año deberá ser inferior a 2299");
            }
            return p;
        }
        #endregion

        #region Propiedades públicas

     

        public DateTime MiercolesCeniza
        {
            get { return SabadoSanto.AddDays(7 * -6 - 3); }
        }

        public DateTime ViernesDolores
        {
            get { return pascuaResurreccion.AddDays(-9); }
        }

        public DateTime DomingoRamos
        {
            get { return pascuaResurreccion.AddDays(-7); }
        }

        public DateTime JuevesSanto
        {
            get { return pascuaResurreccion.AddDays(-3); }
        }

        public DateTime ViernesSanto
        {
            get { return pascuaResurreccion.AddDays(-2); }
        }

        public DateTime SabadoSanto
        {
            get { return pascuaResurreccion.AddDays(-1); }
        }

        public DateTime DomingoResurreccion
        {
            get { return pascuaResurreccion; }
        }

        #endregion

    }
}

Validar un número de cuenta bancaria

En el blog anterior tenía una serie de utilidades que intentaré revisar y traspasar al actual. En esta ocasión le ha tocado el turno a un clásico: la validación de un número de cuenta bancaria:

using System;
using System.Text.RegularExpressions;

namespace JnSoftware.Validaciones
{

    /// <summary>
    /// Servicios de validación de las cuentas bancarias españolas
    /// </summary>
    public static class CuentasBancarias
    {

        /// <summary>
        /// Validación de una cuenta bancaria española
        /// </summary>
        /// <param name="banco">Código del banco en formato "0000"</param>
        /// <param name="oficina">Código de la sucursal en formato "0000"</param>
        /// <param name="dc">Dígito de control en formato "00"</param>
        /// <param name="cuenta">Número de cuenta en formato "0000000000"</param>
        /// <returns>true si el número de cuenta es correcto</returns>
        public static bool ValidaCuentaBancaria(string banco, string oficina, 
                                                        string dc, string cuenta)
        {
            /*
             * Se comprueba que realmente sean números los datos pasados 
             * como parámetros y que las longitudes sean correctas 
             */
            if (!IsNumeric(banco) || banco.Length != 4)
                throw new ArgumentException
                    ("El banco no tiene un formato adecuado");

            if (!IsNumeric(oficina) || oficina.Length != 4)
                throw new ArgumentException
                    ("La oficina no tiene un formato adecuado");

            if (!IsNumeric(dc) || dc.Length != 2)
                throw new ArgumentException
                    ("El dígito de control no tiene un formato adecuado");

            if (!IsNumeric(cuenta) || cuenta.Length != 10)
                throw new ArgumentException
                    ("El número de cuenta no tiene un formato adecuado");

            return CompruebaCuenta(banco, oficina, dc, cuenta);
        }

        /// <summary>
        /// Validación de una cuenta bancaria española
        /// </summary>
        /// <param name="cuentaCompleta">Número de cuenta completa con 
        /// carácteres numéricos y 20 dígitos</param>
        /// <returns>true si el número de cuenta es correcto</returns>
        public static bool ValidaCuentaBancaria(string cuentaCompleta)
        {
            // Comprobaciones de la cadena
            if (cuentaCompleta.Length != 20)
                throw new ArgumentException
                    ("El número de cuenta no el formato adecuado");

            string banco = cuentaCompleta.Substring(0, 4);
            string oficina = cuentaCompleta.Substring(4, 4);
            string dc = cuentaCompleta.Substring(8, 2);
            string cuenta = cuentaCompleta.Substring(10, 10);

            return ValidaCuentaBancaria(banco, oficina, dc, cuenta);

        }

        /// <summary>
        /// Validación de una cuenta bancaria española
        /// </summary>
        /// <param name="banco">Código del banco</param>
        /// <param name="oficina">Código de la oficina</param>
        /// <param name="dc">Dígito de control</param>
        /// <param name="cuenta">Número de cuenta</param>
        /// <returns>true si el número de cuenta es correcto</returns>
        public static bool ValidaCuentaBancaria(UInt64 banco, UInt64 oficina, 
                    UInt64 dc, UInt64 cuenta)
        {
            return ValidaCuentaBancaria(
                                banco.ToString("0000")
                                , oficina.ToString("0000")
                                , dc.ToString("00")
                                , cuenta.ToString("0000000000")
                                );
        }

        /// <summary>
        /// Comprueba que la cadena sólo incluya números
        /// </summary>
        /// <param name="numero">Cadena de texto en formato número</param>
        /// <returns>true si <paramref name="numero"/> contiene sólo números</returns>
        /// <remarks>No se contemplan decimales</remarks>
        private static bool IsNumeric(string numero)
        {
            Regex regex = new Regex("[0-9]");
            return regex.Match(numero).Success;
        }

        /// <summary>
        /// Una cuenta bancaria está validada si los dígitos de control 
        /// calculados coinciden con los que se han pasado en los argumentos
        /// </summary>
        private static bool CompruebaCuenta(string banco, string oficina, 
            string dc, string cuenta)
        {
            return GetDigitoControl("00" + banco + oficina) 
                + GetDigitoControl(cuenta) == dc;
        }

        /// <summary>
        /// Obtiene el dígito de control de una cuenta bancaria. 
        /// La función sólo devuelve un número que corresponderá a una de las dos opciones.
        ///     - Codigo + CódigoOficina
        ///     - CuentaBancaria
        /// </summary>
        private static string GetDigitoControl(string CadenaNumerica)
        {
            int[] pesos = { 1, 2, 4, 8, 5, 10, 9, 7, 3, 6 };
            UInt32 suma = 0;
            UInt32 resto;

            for (int i = 0; i < pesos.Length; i++)
            {
                suma += (UInt32)pesos[i] * 
                    UInt32.Parse(CadenaNumerica.Substring(i, 1));
            }
            resto = 11 - (suma % 11);

            if (resto == 10) resto = 1;
            if (resto == 11) resto = 0;

            return resto.ToString("0");
        }
    }
}

C#: Implementar la Interface ICloneable

No será la primera vez que me he encontrado en la tesitura de tener que duplicar todos los datos de una clase. Quizás, el ejemplo más claro lo encontremos cuando tenemos un grid creado y el usuario te sugiere la posibilidad de crear una nueva función que permita copiar una fila ya existente, con todos sus datos. De esta manera, modificando sólo los datos que varían, la creación de la nueva fila es mucho más sencilla. Reconozco que en alguna ocasión he llegado a implementar la Interface ICloneable de un modo tan rupestre como es la creación de una nueva clase y, propiedad a propiedad, ir haciendo una copia de los valores de la misma. Si hablamos de clases con pocas propiedades y con la premisa que nos caracteriza a casi todos los programadores (¡hay prisa en entregar!), esta acción, sin llegar a ser muy ortodoxa, tiene una justificación, pero cuando la clase ya tiene un número considerable de propiedades o es una clase que varía constantemente según las “exigencias del guión”, no dejará de ser un “coladero de errores”.

Vamos a intentar, en esta ocasión, aprender un poco más de la implementación de la interface ICloneable con el objetivo de poder obtener un rendimiento mejorado.

De hecho, si nuestro objetivo es obtener una copia superficial de una clase, no haría falta la implementación de la interface mencionada, puesto que la clase object ya contiene un método protegido que nos proporciona una “copia” de la clase. Por ejemplo:

using System;

namespace JnSoftware.Test
{
    class Coche
    {
        /// <summary>Obtiene la matrícula de un coche</summary>
        public string Matricula { get; private set; }

        /// <summary>Constructor de la clase.</summary>
        /// <param name="matricula">Matrícula del vehículo</param>
        public Coche(string matricula)
        {
            Matricula = matricula;
        }

        /// <summary>
        /// Ejecución del ejemplo
        /// </summary>
        public static void Main()
        {
            Coche c1 = new Coche("0000AAA");
            Coche c2 = (Coche)c1.MemberwiseClone();

            Console.WriteLine(c1.Matricula);
            Console.WriteLine(c2.Matricula);

            Console.ReadKey();
        }

    }
}

Debemos fijarnos en el método MemberwiseClone(), el cual realiza la función que andábamos buscando. ¿Dónde está entonces el problema?. Es muy sencillo. A la clase que estamos usando de ejemplo, vamos a añadirle algún tipo de funcionalidad extra, de tal manera que se asemeje más a un ejemplo de la vida real que no a un ejercicio básico de programación. En este caso, vamos a incluir las multas que pueda llegar a tener un coche.

Clase: Multas

using System;

namespace JnSoftware.Test
{

    /// <summary>
    /// Clase que representa la multa que tiene un coche determinado.
    /// </summary>
    internal class Multa
    {
        public DateTime Fecha { get; set; }
        public decimal Importe { get; set; }

    }
}

Clase: Coche

using System;
using System.Collections.Generic;
using System.Linq;

namespace JnSoftware.Test
{
    public class Coche
    {
        /// <summary>Obtiene la matrícula de un coche</summary>
        public string Matricula { get; private set; }

        private List<Multa> listaMultas;

        /// <summary>Constructor de la clase.</summary>
        /// <param name="matricula">Matrícula del vehículo</param>
        public Coche(string matricula)
        {
            listaMultas = new List<Multa>();
            Matricula = matricula;
        }

        /// <summary>
        /// Añade una multa
        /// </summary>
        public void AddMulta(DateTime fechaMulta, decimal importe)
        {
            listaMultas.Add(new Multa() { Fecha = fechaMulta, Importe = importe });
        }

        /// <summary>
        /// Obtiene el total de multas de un coche
        /// </summary>
        public decimal GetImporteMultas()
        {
            return listaMultas.Sum(m => m.Importe);
        }

        /// <summary>
        /// Ejecución del ejemplo
        /// </summary>
        public static void Main()
        {
            Coche c1 = new Coche("0000AAA");
            c1.AddMulta(DateTime.Today, 1000);
            
            Coche c2 = (Coche)c1.MemberwiseClone();
            c2.Matricula = "1111BBB";
            c2.AddMulta(DateTime.Today, 1000);


            Console.WriteLine("Coche: {0}tMultas:{1}",c1.Matricula,c1.GetImporteMultas());
            Console.WriteLine("Coche: {0}tMultas:{1}", c2.Matricula, c2.GetImporteMultas());

            Console.ReadKey();
        }
    }
}

Si ejecutamos el ejemplo nos llevaremos una sorpresa. Hemos incluido una multa en el coche c1, con un importe de 1000. En la instancia que hemos duplicado, vemos que la segunda multa, también de 1000 se ha añadido al coche c2, en cambio, a la hora de obtener el resultado del importe de las multas que han tenido ambos vehículos, vemos que los dos tienen 2000 como suma del importe de sus multas. ¿Qué ha pasado?; sencillamente, que no hemos leído en profundidad la ayuda del método MemberwiseClone(), sobre todo, cuando hablaba de una “copia superficial”. Así pues, ¿qué es una “copia superficial”?:

Si un campo es de un tipo de valor, se realiza una copia bit a bit de él.Si un campo es un tipo de referencia, se copia la referencia pero no el objeto al que se hace referencia; por consiguiente, el objeto original y su copia hacen referencia al mismo objeto

    Al haber utilizado, por ejemplo, una List<Multas>, que es un objeto por referencia, vemos que no se ha duplicado su contenido sino que se ha hecho uso de la referencia. Es por eso que el resultado de c1.GetImporteMultas() es el mismo que c2.GetImporteMultas(), aunque la multa de c2 se haya introducido después de haber hecho el “duplicado”.

    Ahora ya conocemos el problema. Vamos a ver cómo podemos resolverlo. Una opción pasaría por hacer una clonación superficial de la clase y, para aquellos elementos que precisen de una intervención manual, podemos, por ejemplo, crear una serie de extensores que permitan la creación de elementos nuevos. En el caso del campo listaMultas, por ejemplo, podríamos crear un método extensor de la clase List<T>:

    using System.Collections.Generic;
    using System.Linq;
    
    namespace JnSoftware.Extensions
    {
        public static class MetodosExtensores
        {
            
            /// <summary>
            /// Clona una clase List
            /// </summary>
            public static List<T> Clone<T>(this List<T> lista) 
            {
                return lista.ToList();
            }
    
        }
    }
     

    De esta manera, una vez referenciado el namespace JnSoftware.Extensions en la clase Coche, bastará con modificar el método Clone de la misma:

    public object Clone()
    {
        // Obtenemos una copia superficial de la clase
        Coche nuevo = (Coche)this.MemberwiseClone();
       
        // Clonación manual de campos
        nuevo.listaMultas = this.listaMultas.Clone();
        return nuevo;
    }

    Claro, visto así, no es que sea una clonación muy “profunda” de los elementos de la clase. Cuanto menos, nada automática ni generalizable, sobre todo si nuestra aplicación hace uso de Colecciones, Listas, Diccionarios y demás contenedores por doquier.

    A partir de ahora, reconozco que hubiese sido incapaz de ver una solución más o menos práctica del problema, sobre todo, cuando mis derroteros iban rascando las clases del namespace System.Reflection, con las propiedades y métodos de la clase Type. Menos mal que San Google nos ayuda en nuestros momentos de extrema depresión. Así, una visita a la página http://es.debugmodeon.com/articulo/clonar-objetos-de-estructura-compleja me ha permitido ver cómo cualquier clase Serializable puede clonarse serializando primero en memoria y deserializando después. Por tanto, es cuestión de crear una clase auxiliar que nos permita tener un método útil para poder clonar clases con estructuras complejas:

    using System;
    using System.IO;
    using System.Runtime.Serialization;
    using System.Runtime.Serialization.Formatters.Binary;
    
    namespace JnSoftware
    {
        public static class Utiles
        {
            /// <summary>
            /// Permite una clonación en profundidad de origen
            /// </summary>
            /// <param name="origen">Objeto serializable</param>
            /// <exception cref="ArgumentExcepcion">
            /// Se produce cuando el objeto no es serializable.
            /// </exception>
            /// <remarks>Extraido de 
            /// http://es.debugmodeon.com/articulo/clonar-objetos-de-estructura-compleja
            /// </remarks>
            public static T Copia<T>(T origen)
            {
                // Verificamos que sea serializable antes de hacer la copia            
                if (!typeof(T).IsSerializable)
                    throw new ArgumentException("La clase " + typeof(T).ToString() + " no es serializable");
                
                // En caso de ser nulo el objeto, se devuelve tal cual
                if (Object.ReferenceEquals(origen, null))
                    return default(T);
                
                //Creamos un stream en memoria            
                IFormatter formatter = new BinaryFormatter();
                Stream stream = new MemoryStream();
                using (stream)
                {
                    try
                    {
                        formatter.Serialize(stream, origen);
                        stream.Seek(0, SeekOrigin.Begin);
                        //Deserializamos la porcón de memoria en el nuevo objeto                
                        return (T)formatter.Deserialize(stream);
                    }
                    catch (SerializationException ex)
                    { throw new ArgumentException(ex.Message, ex); }
                    catch { throw; }
                }
            }
        }
    }
     

    Eso sí, para todas aquellas clases que queramos Clonar con este método, deberemos añadir el atributo Serializable:

    [Serializable()]
    public class Coche : ICloneable

    Ahora, el método Clone de cualquiera de las clases que queramos “clonar”, será tan sencillo de escribir como el método MemberwiseClone()  que usamos al principio:

    public object Clone()
    {
        return JnSoftware.Utiles.Copia(this);
    }

    Confiemos que ya no tenga que ir copiando más las propiedades de una clase…

    ASP.Net: Crear un control de servidor. Ejemplo: JnTextBoxNif

    En el post anterior, Validar CIF, NIF, NIE me comprometí a explicar cómo hacer uso de la clase JnSoftware.Validaciones.NumeroNif para validar un documento introducido en una página ASP.NET. Debo confesar que el motivo inicial era crear un control de usuario en el que se validase dicho documento embebido dentro de un UpdatePanel. Como hasta el momento no había creado nunca un control de servidor, reconozco que ha podido más la curiosidad que otra cosa y al final, después de algún que otro problema, he creado un control de servidor que permite validar y normalizar un documento, sea CIF, NIF o NIE. De este modo, según mi parecer, nos encontramos con un mayor nivel de reusabilidad.

    Antes de nada me gustaría comentar que en las aplicaciones que creo siempre dejo que se introduzca cualquier número en el campo NIF. Sencillamente, lo que hago es que, cuando no es un documento validado lo resalto con un color rojo de fondo que traspasa al usuario la responsabilidad de aceptar o no la entrada.

    Pongámonos manos a la obra. Lo primero que deberemos crear es un proyecto que incluya los controles de servidor. En el caso que nos conlleva ahora sólo introduciremos el control JnTextBoxNif, pero ahora que empiezo a verle la utilidad a este tipo de controles, seguro que no será el único.

    image

    Automáticamente, VisualStudio crea, dentro del proyecto, una clase ServerControl1.cs con la estructura básica. Yo, lo que suelo hacer es eliminar este control y crear uno nuevo. Para ello, le indico que quiero crear un nuevo Control de Servidor.

    image

    No debemos olvidar incluir un par de referencias en el proyecto, por un lado, la referencia a la DLL creada en el post anterior que incluirá la validación del número CIF, NIF, NIE (JnSoftware.Validaciones) y, por otro, como haremos uso de un control UpdatePanel para que se actualice sólo el contenido del control de usuario a la librería System.Web.Extensions, que incluye, entre otros, dicho UpdatePanel.

    using System;
    using System.ComponentModel;
    using System.Drawing;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    using JnSoftware.Validaciones;
    
    /* Asigna el nombre automáticamente de la etiqueta del control */
    [assembly: TagPrefix("JnSoftware.WebControls", "JnControl")]
    
    namespace JnSoftware.WebControls
    {
        [ToolboxData("<{0}:JnTextBoxNif runat=server></{0}:JnTextBoxNif>")]
        [Designer("JnSoftware.WebControls.JnTextBoxNifControlDesigner, JnSoftware.WebControls")]
        [Description("Permite la validación de un NIF,CIF,NIE")]
        [DefaultProperty("Text")]
        public class JnTextBoxNif : WebControl
        {
    
            #region Campos
            // Texbox sobre el que se validará el documento
            TextBox txtNif = new TextBox();
    
            // UpdatePanel para que se actualice sólo el control
            UpdatePanel updatePanel = new UpdatePanel();
            #endregion
    
            
            #region Control en modo de diseño
    
            /* En modo de diseño, se mostrará un Input de HTML */
            private string renderHtml = "<input type="text"  value="{0}"/>";
            
            internal string RenderHtml
            {
                get { return string.Format(renderHtml, txtNif.ID); }
            }
            
            #endregion
    
    
            [Bindable(true), Category("Appearence")]
            public string Text
            {
                get { return txtNif.Text; }
                set { txtNif.Text = value; compruebaNif(); }
            }
    
    
            protected override void OnLoad(EventArgs e)
            {
                controlesPanel();
                base.OnLoad(e);
            }
    
            
            /// <summary>
            /// Carga los elementos del control 
            /// </summary>
            private void controlesPanel()
            {
                // Textbox sobre el que se realizará la validación
                txtNif.ID = ID + "_txtNif";
                txtNif.AutoPostBack = true;
                txtNif.TextChanged += new EventHandler(txtNif_TextChanged);
    
                // UpdatePanel
                Control container = updatePanel.ContentTemplateContainer;
                
                 // Si no se establece el modo de actualización del UpdatePanel 
                 // como Conditional, al actualizar un updatePanel en la página, 
                 // se actualizan todos 
                updatePanel.UpdateMode = UpdatePanelUpdateMode.Conditional;
                updatePanel.RenderMode = UpdatePanelRenderMode.Inline;
    
                // Se añaden los controles
                container.Controls.Add(txtNif);
                base.Controls.Add(updatePanel);
            }
    
    
    
            void txtNif_TextChanged(object sender, EventArgs e)
            {
                compruebaNif();
            }
    
    
            #region Cálculos internos
    
            /// <summary>
            /// Comprueba que el Nif introducido sea correcto.
            /// </summary>
            private void compruebaNif()
            {
                string nif = txtNif.Text;
                try
                {
                    if (!string.IsNullOrEmpty(nif))
                    {
                        NumeroNif numero = NumeroNif.CompruebaNif(nif);
                        if (!numero.EsCorrecto)
                            throw new ApplicationException("Número no válido");
                        txtNif.Text = numero.ToString();
                    }
                    formatoTextBox(true);
                }
                catch
                {
                    formatoTextBox(false);
                }
            }
    
    
            /// <summary>
            /// Aplica un formato personalizado al TextBox en base 
            /// al resultado de la validación del documento
            /// </summary>
            /// <param name="validado">True si es un documento correcto</param>
            private void formatoTextBox(bool validado)
            {
                Color colorFondo = Color.Empty;
                if (!validado)
                    colorFondo = Color.Red;
                txtNif.BackColor = colorFondo;
            }
    
            #endregion
        }
    }
     

    Aunque he incluido la clase una vez finalizada, se puede apreciar que no reviste gran dificultad, puesto que son pocos los métodos que realmente tienen una gran trascendencia en el conjunto.

    • controlesPanel, que realiza la carga de los elementos que se incluirán dentro del UpdatePanel (en el ejemplo tan sólo existe el TextBox que nos permitirá introducir y validar el NIF, CIF, NIE).
    • compruebaNif, que es el método responsable de comprobar el documento introducido y, gracias a formatoTextBox, de aplicar un formato destacado en caso de no ser válido.

    Ahora bien, una vez finalizada la clase, ya la veo hasta bonita, pero reconozco que me ha costado un buen rato descubrir los puntos siguientes:

    • Si creaba, dentro de una misma página varios controles JnTextBoxNif, en el momento que actualizaba uno, se actualizaban también todos los demás. Esto es consecuencia del modo de actualización de los diferentes UpdatePanel (uno por cada control). El modo de solventar este problema es cambiar el modo de actualización del UpdatePanel a Conditional.
    • Quizás sea una tontería, pero personalmente suele molestarme mucho que, cuando añado controles personales en una página, si me limito a arrastrar el componente desde la pantalla de diseño, VisualStudio le asigna un prefijo automático del tipo TagPrefix=”cc1”. Solvento este problema con la etiqueta [assembly] para que todos los controles tengan el mismo prefijo en la página.
    • Otra sorpresa: aunque el control funciona perfectamente en tiempo de ejecución, resulta que en vista de diseño no se muestra absolutamente nada. Para “mostrar algo” cuando esté trabajando en el modo de diseño me fijo en la clase System.Web.UI.Design.ControlDesigner, que me obliga a añadir una referencia nueva, System.Design. Una vez añadida dicha referencia, heredo de la clase ControlDesigner para que se muestre en tiempo de diseño un elemento similar al del TextBox (un Input de HTML, por ejemplo).
    using System;
    
    namespace JnSoftware.WebControls
    {
        class JnTextBoxNifControlDesigner : System.Web.UI.Design.ControlDesigner
        {
            public override string GetDesignTimeHtml()
            {
                JnTextBoxNif t = (JnTextBoxNif)Component;
                return t.RenderHtml;
            }
        }
    }

     

    Cuando ya tenemos el control diseñado es hora de probarlo, así que creo un nuevo proyecto Web dentro de la misma solución y le añado la referencia correspondiente del proyecto JnSoftware.WebControls. Aunque añadir un nuevo control es tan sólo arrastrar el control que nos encontramos en el cuadro de herramientas, dejo constancia del código de la página puesto que así, evito olvidarme de recordar que debemos añadir un ScriptManager en la página para que funcione correctamente el UpdatePanel:

    <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="TestNif.aspx.cs" Inherits="WebPresentacion.TestNif" %>
    <%@ Register Assembly="JnSoftware.WebControls" Namespace="JnSoftware.WebControls" TagPrefix="JnControl" %>
    
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head runat="server">
        <title></title>
    </head>
    <body>
        <form id="form1" runat="server">
        <asp:ScriptManager ID="sm" runat="server" />
        <div>
            NIF 1: <JnControl:JnTextBoxNif ID="JnTextBoxNif1" runat="server" /><br />
            NIF 2: <JnControl:JnTextBoxNif ID="JnTextBoxNif2" runat="server" /><br />
        </div>
        </form>
    </body>
    </html>

    C#. Validar CIF, NIF, NIE

    Imagino que, al igual que todos los que alguna vez hemos empezado en esto del desarrollo de aplicaciones, nuestro ego nos ha jugado alguna que otra mala pasada. Ya desde mis inicios de la mano de Access o de Visual Basic 3.0, veía con bastante desgana cómo se iban introduciendo CIFs del tipo A00000000 o A-00000000, o A-00.000.000; matrículas con formatos dispares: GGG0000 ó 0000GGG. Comento y quizás resalto “desgana”, puesto que es uno de los puntos menos valorados tanto por desarrolladores como por clientes. Si total, sólo vale para perder el tiempo y retrasar la finalización del programa. Claro, que el problema nos lo encontramos nosotros cuando llega el momento de obtener un listado ordenado o una simple búsqueda. No recuerdo si fue la versión 5 de VB la que ya incluía la posibilidad de crear OCX que, entre otras cosas, permitía separar un conjunto de controles con el objetivo de reutilizarlos del código de la aplicación.

    El caso es que he tenido miles de peleas con los datos cuando éstos no estaban normalizados, así que un día me levanté y me puse manos a la obra con la finalidad de intentar homologar todos aquellos datos que sean susceptibles de tener un “formato igual”. En el blog anterior ya quedó alguno de estos intentos. Una de las rutinas que más me costó en su día fue el intento de poder validar, en un mismo campo, un CIF de empresa, un NIF de persona física y el Número de Identificación de Extranjería (NIE), sin tener que indicarle previamente el tipo de documento.

    Como suele pasar cada vez que se empieza un nuevo proyecto, el primer boceto contenía múltiples errores. Algunos de ellos se fueron subsanando; otros, quizás por la falta de tiempo habitual, no quedaron pulidos del todo. Así, aprovechando el haber tenido un poco de tiempo estas fiestas, he intentado pulir el código, de tal manera que sea útil para cualquiera que lo precise.

    Uno de los errores que tenía era la eliminación, en la normalización, de aquellos números que empezaban por 0 y la asignación de algún NIE cuando realmente no lo era. Lo único que he omitido de los comentarios que me hicieron en su día es un tema sobre el que anduve buscando información y no supe encontrar una solución bien documentada: los NIE están formados por una letra, ocho dígitos numéricos y el dígito de control. En su lugar, he optado por tomar la definición y la normativa publicada en Wikipedia a tal respecto.

    Dejo el código tal cuál ha quedado ya reformado:

    using System;
    using System.Text.RegularExpressions;
    
    /*
     * NumeroNif.cs
     * Servicios de validación de los números NIF
     * 
     * älskare, Ene/11
     */
    
    namespace JnSoftware.Validaciones
    {
    
        /// <summary>
        /// Representa un número. En la clase se desglosan las distintas opciones que se puedan
        /// encontrar
        /// </summary>
        public class NumeroNif
        {
            /// <summary>
            /// Tipos de Códigos.
            /// </summary>
            /// <remarks>Aunque actualmente no se utilice el término CIF, se usa en la enumeración
            /// por comodidad</remarks>
            private enum TiposCodigosEnum { NIF, NIE, CIF }
    
            // Número tal cual lo introduce el usuario
            private string numero;
            private TiposCodigosEnum tipo;
    
            /// <summary>
            /// Parte de Nif: En caso de ser un Nif intracomunitario, permite obtener el cógido del país
            /// </summary>
            public string CodigoIntracomunitario { get; internal set; }
            internal bool EsIntraComunitario { get; set; }
    
            /// <summary>
            /// Parte de Nif: Letra inicial del Nif, en caso de tenerla
            /// </summary>
            public string LetraInicial { get; internal set; }
    
            /// <summary>
            /// Parte de Nif: Bloque numérico del NIF. En el caso de un NIF de persona física,
            /// corresponderá al DNI
            /// </summary>
            public int Numero { get; internal set; }
    
            /// <summary>
            /// Parte de Nif: Dígito de control. Puede ser número o letra
            /// </summary>
            public string DigitoControl { get; internal set; }
    
            /// <summary>
            /// Valor que representa si el Nif introducido es correcto
            /// </summary>
            public bool EsCorrecto { get; internal set; }
    
            /// <summary>
            /// Cadena que representa el tipo de Nif comprobado:
            ///     - NIF : Número de identificación fiscal de persona física
            ///     - NIE : Número de identificación fiscal extranjería
            ///     - CIF : Código de identificación fiscal (Entidad jurídica)
            /// </summary>
            public string TipoNif { get { return tipo.ToString(); } }
    
            /// <summary>
            /// Constructor. Al instanciar la clase se realizan todos los cálculos
            /// </summary>
            /// <param name="numero">Cadena de 9 u 11 caracteres que contiene el DNI/NIF
            /// tal cual lo ha introducido el usuario para su verificación</param>
            private NumeroNif(string numero)
            {
                // Se eliminan los carácteres sobrantes
                numero = EliminaCaracteres(numero);
    
                // Todo en maýusculas
                numero = numero.ToUpper();
    
                // Comprobación básica de la cadena introducida por el usuario
                if (numero.Length != 9 && numero.Length != 11)
                    throw new ArgumentException("El NIF no tiene un número de caracteres válidos");
               
                this.numero = numero;
                Desglosa();
    
                switch (tipo)
                {
                    case TiposCodigosEnum.NIF:
                    case TiposCodigosEnum.NIE:
                        this.EsCorrecto = CompruebaNif();
                        break;
                    case TiposCodigosEnum.CIF:
                        this.EsCorrecto = CompruebaCif();
                        break;
                }
            }
    
            #region Preparación del número (desglose)
    
            /// <summary>
            /// Realiza un desglose del número introducido por el usuario en las propiedades
            /// de la clase
            /// </summary>
            private void Desglosa()
            {
                Int32 n;
                if (numero.Length == 11)
                {
                    // Nif Intracomunitario
                    EsIntraComunitario = true;
                    CodigoIntracomunitario = numero.Substring(0, 2);
                    LetraInicial = numero.Substring(2, 1);
                    Int32.TryParse(numero.Substring(3, 7), out n);
                    DigitoControl = numero.Substring(10, 1);
                    tipo = GetTipoDocumento(LetraInicial[0]);
                }
                else
                {
                    // Nif español
                    tipo = GetTipoDocumento(numero[0]);
                    EsIntraComunitario = false;
                    if (tipo == TiposCodigosEnum.NIF)
                    {
                        LetraInicial = string.Empty;
                        Int32.TryParse(numero.Substring(0, 8), out n);
                    }
                    else
                    {
                        LetraInicial = numero.Substring(0, 1);
                        Int32.TryParse(numero.Substring(1, 7), out  n);
                    }
                    DigitoControl = numero.Substring(8, 1);
                }
                Numero = n;
            }
    
            /// <summary>
            /// En base al primer carácter del código, se obtiene el tipo de documento que se intenta
            /// comprobar
            /// </summary>
            /// <param name="letra">Primer carácter del número pasado</param>
            /// <returns>Tipo de documento</returns>
            private TiposCodigosEnum GetTipoDocumento(char letra)
            {
                Regex regexNumeros = new Regex("[0-9]");
                if (regexNumeros.IsMatch(letra.ToString()))
                    return TiposCodigosEnum.NIF;
    
                Regex regexLetrasNIE = new Regex("[XYZ]");
                if (regexLetrasNIE.IsMatch(letra.ToString()))
                    return TiposCodigosEnum.NIE;
    
                Regex regexLetrasCIF = new Regex("[ABCDEFGHJPQRSUVNW]");
                if (regexLetrasCIF.IsMatch(letra.ToString()))
                    return TiposCodigosEnum.CIF;
    
                throw new ApplicationException("El código no es reconocible");
            }
    
    
    
            /// <summary>
            /// Eliminación de todos los carácteres no numéricos o de texto de la cadena
            /// </summary>
            /// <param name="numero">Número tal cual lo escribe el usuario</param>
            /// <returns>Cadena de 9 u 11 carácteres sin signos</returns>
            private string EliminaCaracteres(string numero)
            {
                // Todos los carácteres que no sean números o letras
                string caracteres = @"[^w]";
                Regex regex = new Regex(caracteres);
                return regex.Replace(numero, "");
            }
    
            #endregion
    
            #region Cálculos
    
            private bool CompruebaNif()
            {
                return DigitoControl == GetLetraNif();
            }
    
            /// <summary>
            /// Cálculos para la comprobación del Cif (Entidad jurídica)
            /// </summary>
            private bool CompruebaCif()
            {
                string[] letrasCodigo = { "J", "A", "B", "C", "D", "E", "F", "G", "H", "I" };
    
                string n = Numero.ToString("0000000");
                Int32 sumaPares = 0;
                Int32 sumaImpares = 0;
                Int32 sumaTotal = 0;
                Int32 i = 0;
                bool retVal = false;
    
                // Recorrido por todos los dígitos del número
                for (i = 0; i < n.Length; i++)
                {
                    Int32 aux;
                    Int32.TryParse(n[i].ToString(), out aux);
    
                    if ((i + 1) % 2 == 0)
                    {
                        // Si es una posición par, se suman los dígitos
                        sumaPares += aux;
                    }
                    else
                    {
                        // Si es una posición impar, se multiplican los dígitos por 2 
                        aux = aux * 2;
    
                        // se suman los dígitos de la suma
                        sumaImpares += SumaDigitos(aux);
                    }
                }
                // Se suman los resultados de los números pares e impares
                sumaTotal += sumaPares + sumaImpares;
    
                // Se obtiene el dígito de las unidades
                Int32 unidades = sumaTotal % 10;
    
                // Si las unidades son distintas de 0, se restan de 10
                if (unidades != 0)
                    unidades = 10 - unidades;
    
                switch (LetraInicial)
                {
                    // Sólo números
                    case "A":
                    case "B":
                    case "E":
                    case "H":
                        retVal = DigitoControl == unidades.ToString();
                        break;
    
                    // Sólo letras
                    case "K":
                    case "P":
                    case "Q":
                    case "S":
                        retVal = DigitoControl == letrasCodigo[unidades];
                        break;
    
                    default:
                        retVal = (DigitoControl == unidades.ToString())
                                || (DigitoControl == letrasCodigo[unidades]);
                        break;
                }
    
                return retVal;
    
            }
    
            /// <summary>
            /// Obtiene la suma de todos los dígitos
            /// </summary>
            /// <returns>de 23, devuelve la suma de 2 + 3</returns>
            private Int32 SumaDigitos(Int32 digitos)
            {
                string sNumero = digitos.ToString();
                Int32 suma = 0;
    
                for (Int32 i = 0; i < sNumero.Length; i++)
                {
                    Int32 aux;
                    Int32.TryParse(sNumero[i].ToString(), out aux);
                    suma += aux;
                }
                return suma;
            }
    
            /// <summary>
            /// Obtiene la letra correspondiente al Dni
            /// </summary>
            private string GetLetraNif()
            {
                int indice = Numero % 23;
                return "TRWAGMYFPDXBNJZSQVHLCKET"[indice].ToString();
            }
    
            /// <summary>
            /// Obtiene una cadena con el número de identificación completo
            /// </summary>
            public override string ToString()
            {
                string nif;
                string formato = "{0:0000000}";
    
                if (tipo == TiposCodigosEnum.CIF && LetraInicial == "")
                    formato = "{0:00000000}";
                if (tipo == TiposCodigosEnum.NIF)
                    formato = "{0:00000000}";
                
                nif = EsIntraComunitario ? CodigoIntracomunitario :
                    string.Empty + LetraInicial + string.Format(formato,Numero ) + DigitoControl;
                return nif;
            }
    
            #endregion
    
            /// <summary>
            /// Comprobación de un número de identificación fiscal español
            /// </summary>
            /// <param name="numero">Numero a analizar</param>
            /// <returns>Instancia de <see cref="NumeroNif"/> con los datos del número.
            /// Destacable la propiedad <seealso cref="NumeroNif.EsCorrecto"/>, que contiene la verificación
            /// </returns>
            public static NumeroNif CompruebaNif(string numero)
            {
                return new NumeroNif(numero);
            }
    
        }
    
    }
     

    En el siguiente post intentaré comentar cómo podemos crear un control de usuario que nos valide la entrada de un CIF/NIF/NIE correcto en ASP Net. Digo intentaré porque últimamente me estoy oxidando mucho con el tema de los controles de ASP. ¿Cuál es la razón?, muy sencillo, que cada día soy más vago y últimamente me estoy habituando demasiado a trabajar con los controles de DevExpress que, tal como lo veo, ofrecen una gran cantidad de opciones que, de trabajar con controles estándares me costarían bastante de hacer.

    CultureInfo. Personalizar formatos

    Recientemente publiqué un post en el que había creado una clase que facilitaba la labor de crear ficheros delimitados. Como suele ocurrir casi siempre, baso los artículos en vivencias de mi vida laboral. Es por esto que, tal como ha sucedido en el presente, en más de una ocasión, tras resolver un problema, surge otro nuevo.

    El caso es que, hasta el momento, cuando había tenido que crear un fichero de este tipo para enviar al exterior, siempre estaba hablando con empresas españolas. En este caso, es distinto, puesto que el fichero irá a parar a una multinacional que, lógicamente, impone sus normas. En principio, estas normas no suelen dar muchos problemas, pero sí que representan algún trastorno en lo referente a los formatos numéricos y de fechas.

    Reconozco que hasta el momento nunca me había preocupado mucho de estos temas de crear aplicaciones multilingües ni tal siquiera de cambiar determinados formatos, sobre todo numéricos, pero como ha llegado el momento de hacerlo, pues he empezado a interesarme, de momento, por alguna herramienta que nos ofrece el Framework. En esta ocasión no he pasado de la clase CultureInfo, aunque no descarto seguir con las interfaces IFormatProvider e ICustomFormater, que me han dado muy buenas vibraciones.

    Bueno, vamos al grano, que siempre me disipo intentando entrar en situación. El caso es que necesito tener un modo más o menos centralizado en el que pueda definir cómo quedarán los formatos de fecha y numéricos, así que lo mejor que he sabido ver es crear una clase CulturaPropia, en la que pueda personalizar los formatos que necesito. Esta clase heredará de otra del Framework, la clase CultureInfo.

    using System;
    using System.Globalization;
    
    namespace JnSoftware.Samples
    {
        class CulturaPropia : CultureInfo
        {
            public CulturaPropia()
                : base("es-ES")
            {
                setFormatoNumerico();
                setFormatoMoneda();
                setFormatoFecha();
            }
    
            /// <summary>
            /// Establece formatos numéricos
            /// </summary>
            private void setFormatoNumerico()
            {
                NumberFormatInfo n = this.NumberFormat;
                n.NumberNegativePattern = 0;
                n.NumberGroupSeparator = ",";
                n.NumberDecimalSeparator = ".";
            }
    
            /// <summary>
            /// Establece formatos de Moneda
            /// </summary>
            private void setFormatoMoneda()
            {
                NumberFormatInfo n = this.NumberFormat;
                n.CurrencyPositivePattern = 3;
                n.CurrencySymbol = "EUR";
            }
    
            /// <summary>
            /// Establece formatos de fecha
            /// </summary>
            private void setFormatoFecha()
            {
                DateTimeFormatInfo d = this.DateTimeFormat;
                d.LongDatePattern = "'Montcada i Reixac, 'dd' de 'MMMM' de 'yyyy" ;
                d.ShortDatePattern = "dd.MMM.yyyy";
                d.FirstDayOfWeek = DayOfWeek.Monday;
                d.TimeSeparator = "/";
                d.DateSeparator = ".";
            }
        }
    }

    Como se puede apreciar, la clase es bien sencilla, puesto que, desde el mismo constructor, lo que intenta es “heredar” toda la información de una referencia cultural completa (en el ejemplo es-ES, correspondiente a España). A partir de aquí, tan sólo es cuestión de ir modificando aquellas propiedades de la clase que necesitemos.

    Para la ejecución de las pruebas, he creado un proyecto de consola en el cual, sencillamente, muestro una comparación entre tres referencias diferentes: americana, española y la que he creado. Es posible que existan otras opciones de hacerlo, pero he visto que me resulta muy cómodo cambiar la referencia cultural al Thread actual, extraer los datos y volver a dejar tal cuál estaba la referencia.

    using System;
    using System.Globalization;
    
    namespace JnSoftware.Samples
    {
        class EjecucionPruebas
        {
            // Muestra diversa información en pantalla.
            public void MuestraEjemplo(string titulo, CultureInfo cultura)
            {
                DateTime fecha = DateTime.Now;
                double numero = 12345.6789;
    
                System.Threading.Thread.CurrentThread.CurrentCulture = cultura;
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.WriteLine(new string('=', Console.WindowWidth - 1));
                Console.WriteLine(titulo);
                Console.WriteLine(new string('=', Console.WindowWidth - 1));
                Console.ResetColor();
                Console.WriteLine("Fecha 1  : t{0:d}", fecha);
                Console.WriteLine("Fecha 2  : t{0:D}", fecha);
                Console.WriteLine("General  : t{0:G}", numero);
                Console.WriteLine("Moneda   : t{0:C}", numero);
                Console.WriteLine("Decimal  : t{0:D}", (int)numero);
                Console.WriteLine("Número   : t{0:N}", numero);
                Console.WriteLine("P. Fijo  : t{0:F}", numero);
            }
    
            // Ejecución de las pruebas.
            public static void Main()
            {
                // Lectura de la referencia actual
                CultureInfo actual = System.Threading.Thread.CurrentThread.CurrentCulture;
                Console.WindowHeight = 31;
                EjecucionPruebas test = new EjecucionPruebas();
                // Cambios de referencia culturales 
                test.MuestraEjemplo("en-US", new CultureInfo("en-US"));
                test.MuestraEjemplo("es-ES", new CultureInfo("es-ES"));
                test.MuestraEjemplo("Propia", new CulturaPropia());
                test = null;
                // Se establece la referencia inicial
                System.Threading.Thread.CurrentThread.CurrentCulture = actual;
                Console.ReadKey();
            }
        }
    }

    Tal como puede apreciarse, salvo algún ornamento añadido para que se muestre mejor la pantalla resultante, lo único que se hace en la aplicación es mostrar los mismos datos en las diferentes referencias culturales.

    El resultado será el siguiente:

    image

    C# – Crear ficheros delimitados

    No será la primera vez que en alguna de mis aplicaciones he tenido que crear ficheros de texto para intercambiar información con otras empresas. Para bancos, cajas de ahorro, ayuntamientos y un conjunto grande de entidades es moneda de cambio el trabajar con este tipo de documentos.

    Aunque no es un punto excesivamente complicado, cada vez que he tenido que hacer un fichero ASCII delimitado he creado algún procedimiento que se encargase de este trabajo. Hoy me he propuesto crearme una clase que me permita generalizarme un poco la labor.

    Si cogemos un ejemplo, como puede ser la creación de un fichero de adeudo por domiciliaciones (Norma 19), podríamos resumir diciendo que tiene la siguiente estructura:

    1. cabecera
    2. lista de datos
    3. resumen

    Por tanto, con el fin de facilitar el trabajo, además de los procedimientos que necesitaremos para poder crear el fichero, deberemos tener unos métodos que nos permitan escribir una lista completa de datos o una línea de texto. Algo así como:

    public class CamposDelimitados
     {
         /// <summary>
         /// Permite enviar todos los elementos de una lista al fichero de salida
         /// </summary>
         /// <param name="lista">Lista de elementos que deberán aparecer 
         /// en el fichero de salida</param>
         /// <remarks>La definición de la estructura de salida deberá especificarse 
         /// en el método ToString()</remarks>
         public void InsertaLista(IList lista)
         {
             throw new System.NotImplementedException();
         }
    
    
         /// <summary>
         /// Permite la inserción de una línea individual en el fichero de salida
         /// </summary>
         /// <param name="texto">Texto a incorporar en el fichero de salida</param>
         public void InsertaLinea(string texto)
         {
             throw new System.NotImplementedException();
         }
     }

    Por tanto, una clase que tenga algún tipo de generalización y que sea útil para tal acción podría ser la siguiente:

    using System.Collections;
    using System.IO;
    using System.Text;
    
    
    namespace JnSoftware.Utiles
    {
    
        /// <summary>
        /// Representa un escritor de ficheros delimitados.
        /// </summary>
        public class CamposDelimitados
        {
    
            private StringBuilder textoSalida = new StringBuilder();
    
            /// <summary>
            /// Permite enviar todos los elementos de una lista al 
            /// fichero de salida
            /// </summary>
            /// <param name="lista">Lista de elementos que deberán 
            /// aparecer en el fichero de salida</param>
            /// <remarks>La definición de la estructura de salida 
            /// deberá especificarse en el método ToString()</remarks>
            public void InsertaLista(IList lista)
            {
                foreach (object item in lista)
                    InsertaLinea(item.ToString());
            }
    
    
            /// <summary>
            /// Permite la inserción de una línea.
            /// </summary>
            /// <param name="texto">Texto a incorporar en el fichero 
            /// de salida</param>
            public void InsertaLinea(string texto)
            {
                textoSalida.AppendLine(texto);
            }
    
    
            /// <summary>
            /// Crea el fichero de salida
            /// </summary>
            /// <param name="nombreFichero">Ruta completa del fichero 
            /// que contendrá los datos delimitados</param>
            public void creaFichero(string nombreFichero)
            {
                using (StreamWriter sw =
                    new StreamWriter(nombreFichero, false, Encoding.UTF8))
                {
                    sw.Write(textoSalida);
                    sw.Flush();
                    sw.Close();
                }
            }
        }
    }

    ¿En qué me ayudará esta clase?. Es una pregunta sencilla de responder, puesto que fijándonos en el método InsertaLista, podremos deducir que bastará con crear una implementación propia del método ToString() de nuestras clases para definir la delimitación de los datos.

    Claro que está que la implementación personalizada (o customizada, que últimamente está muy de moda este vocablo) del método ToString() representa un par de problemas adicionales, Por un lado, es algo personal ya que siempre me ha fastidiado bastante que hay empresas que suelen darte la columna inicial y la final y hay otras que te dan la longitud que deberá tener cada columna de datos; por otro lado, está la problemática de hacer que nuestras columnas de datos adquieran una longitud fija. Con la intención de solventar estos dos problemas, creo una serie de métodos estáticos en la misma clase para que agilicen la adaptación de los datos a la cadena resultante.

    namespace JnSoftware.Utiles
    {
    
        /// <summary>
        /// Describe el modo de alineación de un texto dentro 
        /// de un archivo delimitado
        /// </summary>
        public enum TextAlignment { Left, Right }
    
    
        /// <summary>
        /// Representa un escritor de ficheros delimitados.
        /// </summary>
        public class CamposDelimitados
        {
    
            /// <summary>
            /// Permite adaptar el contenido de un campo a un formato de tamaño fijo.
            /// </summary>
            /// <param name="campo">Contenido del campo en formato string</param>
            /// <param name="inicio">Entero que representa la posición 
            /// inicial en la delimitación</param>
            /// <param name="fin">Entero que representa la posición final en 
            /// la delimitación</param>
            /// <returns>Cadena preparada para insertar en el fichero</returns>
            public static string PreparaLinea(string campo, int inicio, int fin)
            {
                return PreparaLinea(campo, inicio, fin, TextAlignment.Left);
            }
    
    
            /// <summary>
            /// Permite adaptar el contenido de un campo a un formato de tamaño fijo.
            /// </summary>
            /// <param name="campo">Contenido del campo en formato string</param>
            /// <param name="longitudCampo">Longitud que deberá tener el campo 
            /// en la delimitación</param>
            /// <returns>Cadena preparada para insertar en el fichero</returns>
            public static string PreparaLinea(string campo, int longitudCampo)
            {
                return PreparaLinea(campo, longitudCampo, TextAlignment.Left);
            }
    
    
            /// <summary>
            /// Permite adaptar el contenido de un campo a un formato de tamaño fijo.
            /// </summary>
            /// <param name="campo">Contenido del campo en formato string</param>
            /// <param name="inicio">Entero que representa la posición inicial 
            /// en la delimitación</param>
            /// <param name="fin">Entero que representa la posición inicial en 
            /// la delimitación</param>
            /// <param name="alineacion">Alineación del texto en el fichero 
            /// delimitado</param>
            /// <returns>Cadena preparada para insertar en el fichero</returns>
            public static string PreparaLinea(
                string campo,
                int inicio,
                int fin,
                TextAlignment alineacion)
            {
                return PreparaLinea(campo, fin - inicio, alineacion);
            }
    
    
            /// <summary>
            /// Permite adaptar el contenido de un campo a un formato de tamaño fijo.
            /// </summary>
            /// <param name="campo">Contenido del campo en formato string</param>
            /// <param name="longitud">Longitud que deberá tener el campo en la 
            /// delimitación</param>
            /// <param name="alineacion">Alineación del texto en el fichero 
            /// delimitado</param>
            /// <returns>Cadena preparada para insertar en el fichero</returns>
            public static string PreparaLinea(
                string campo,
                int longitud,
                TextAlignment alineacion
                )
            {
                string espacios = new string(' ', longitud - campo.Length);
                if (alineacion == TextAlignment.Left)
                    return campo + espacios;
                else
                    return espacios + campo;
            }
        }
    }

    Por tanto, podemos decir que la clase, una vez finalizada, será la siguiente:

    using System.Collections;
    using System.IO;
    using System.Text;
    
    
    namespace JnSoftware.Utiles
    {
    
        /// <summary>
        /// Describe el modo de alineación de un texto dentro 
        /// de un archivo delimitado
        /// </summary>
        public enum TextAlignment { Left, Right }
    
    
        /// <summary>
        /// Representa un escritor de ficheros delimitados.
        /// </summary>
        public class CamposDelimitados
        {
    
            private StringBuilder textoSalida = new StringBuilder();
    
            /// <summary>
            /// Permite enviar todos los elementos de una lista al 
            /// fichero de salida
            /// </summary>
            /// <param name="lista">Lista de elementos que deberán 
            /// aparecer en el fichero de salida</param>
            /// <remarks>La definición de la estructura de salida 
            /// deberá especificarse en el método ToString()</remarks>
            public void InsertaLista(IList lista)
            {
                foreach (object item in lista)
                    InsertaLinea(item.ToString());
            }
    
    
            /// <summary>
            /// Permite la inserción de una línea.
            /// </summary>
            /// <param name="texto">Texto a incorporar en el fichero 
            /// de salida</param>
            public void InsertaLinea(string texto)
            {
                textoSalida.AppendLine(texto);
            }
    
    
            /// <summary>
            /// Crea el fichero de salida
            /// </summary>
            /// <param name="nombreFichero">Ruta completa del fichero 
            /// que contendrá los datos delimitados</param>
            public void creaFichero(string nombreFichero)
            {
                using (StreamWriter sw =
                    new StreamWriter(nombreFichero, false, Encoding.UTF8))
                {
                    sw.Write(textoSalida);
                    sw.Flush();
                    sw.Close();
                }
            }
    
    
            /// <summary>
            /// Permite adaptar el contenido de un campo a un formato de tamaño fijo.
            /// </summary>
            /// <param name="campo">Contenido del campo en formato string</param>
            /// <param name="inicio">Entero que representa la posición 
            /// inicial en la delimitación</param>
            /// <param name="fin">Entero que representa la posición final en 
            /// la delimitación</param>
            /// <returns>Cadena preparada para insertar en el fichero</returns>
            public static string PreparaLinea(string campo, int inicio, int fin)
            {
                return PreparaLinea(campo, inicio, fin, TextAlignment.Left);
            }
    
    
            /// <summary>
            /// Permite adaptar el contenido de un campo a un formato de tamaño fijo.
            /// </summary>
            /// <param name="campo">Contenido del campo en formato string</param>
            /// <param name="longitudCampo">Longitud que deberá tener el campo 
            /// en la delimitación</param>
            /// <returns>Cadena preparada para insertar en el fichero</returns>
            public static string PreparaLinea(string campo, int longitudCampo)
            {
                return PreparaLinea(campo, longitudCampo, TextAlignment.Left);
            }
    
    
            /// <summary>
            /// Permite adaptar el contenido de un campo a un formato de tamaño fijo.
            /// </summary>
            /// <param name="campo">Contenido del campo en formato string</param>
            /// <param name="inicio">Entero que representa la posición inicial 
            /// en la delimitación</param>
            /// <param name="fin">Entero que representa la posición inicial en 
            /// la delimitación</param>
            /// <param name="alineacion">Alineación del texto en el fichero 
            /// delimitado</param>
            /// <returns>Cadena preparada para insertar en el fichero</returns>
            public static string PreparaLinea(
                string campo,
                int inicio,
                int fin,
                TextAlignment alineacion)
            {
                return PreparaLinea(campo, fin - inicio, alineacion);
            }
    
    
            /// <summary>
            /// Permite adaptar el contenido de un campo a un formato de tamaño fijo.
            /// </summary>
            /// <param name="campo">Contenido del campo en formato string</param>
            /// <param name="longitud">Longitud que deberá tener el campo en la 
            /// delimitación</param>
            /// <param name="alineacion">Alineación del texto en el fichero 
            /// delimitado</param>
            /// <returns>Cadena preparada para insertar en el fichero</returns>
            public static string PreparaLinea(
                string campo,
                int longitud,
                TextAlignment alineacion
                )
            {
                string espacios = new string(' ', longitud - campo.Length);
                if (alineacion == TextAlignment.Left)
                    return campo + espacios;
                else
                    return espacios + campo;
            }
        }
    }

    ¿Cómo utilizar la clase?

    Creo un pequeño ejemplo en el que se muestra cómo podemos hacer uso de la clase en la creación de un fichero delimitado. En este ejemplo queda claro cómo definir el método ToString() para que se adapte al formato deseado:

    using System;
    using System.Collections.Generic;
    using JnSoftware.Utiles;
    
    namespace TextosDelimitadosTest
    {
        class CreacionFicheroDelimitado
        {
    
            public int Id { get; set; }
            public string Texto { get; set; }
            public DateTime Fecha { get; set; }
    
            public override string ToString()
            {
                // En el método ToString() indicamos cómo deberá aparecer la información
                return 
                    CamposDelimitados.PreparaLinea(Id.ToString(),1, 7, TextAlignment.Right)
                    + CamposDelimitados.PreparaLinea(Texto, 30)
                    + CamposDelimitados.PreparaLinea(Fecha.ToShortDateString(), 10);
                    ;
            }
    
            // Ejecución de las pruebas
            public static void Main()
            {
                // Fichero de salida
                string fichero = @"C:Usersj.velascoDesktopexitTestsalida.txt";
    
                // Creación de una lista de ejemplo
                List<CreacionFicheroDelimitado> lista = 
                    new List<CreacionFicheroDelimitado>();
                for (int i = 1; i < 20; i++)
                {
                    lista.Add(
                        new CreacionFicheroDelimitado()
                        {
                            Id = i,
                            Texto = "Texto " + i.ToString(), 
                            Fecha = DateTime.Now 
                        }
                        );
                }
    
                // Creación del fichero
                CamposDelimitados cd = new CamposDelimitados();
                cd.InsertaLinea("-- Cabecera del texto");
                cd.InsertaLista(lista);
                cd.creaFichero(fichero);
            }
        }
    }