Everything: Una herramienta productiva.

17 11 2009

Existe una multitud de herramientas, plugins para Visual Studio, editores, y demás que nos asisten a la hora de desarrollar. Yo particularmente soy amigo de las más sencillas, ya bastante con aprender todos los vericuetos del entorno.

Entre estas herramientas que me “conquistaron”, la que más me gusta es Everything, Un buscador de archivos. No hace nada que no se pueda hacer con Windows o con muchas otras herramientas que realizan la misma tarea y ofrecen mucha más funcionalidad, básicamente escribimos partes del nombre del cualquier archivo separadas por espacios y nos muestra una lista de todos los que coinciden con el patrón buscado.

¿Por que vale la pena entonces? El secreto de Everything es que lo que hace lo hace bien, es gratis (vale donar ¡eh!), consume escasos recursos y es muy, muy, MUY rápido.

Busqueda en Everything

El secreto: Respuesta inmediata.

Para ser claro, a la pulsación de cada tecla tendremos nuestra lista de archivos filtrada. Para escanear todo el disco rígido por primera vez solo tarda unos segundos, y los cambios que se hagan a los archivos, afectan los resultados de Everything en tiempo real.

Ahora, si trabajan en proyectos con cientos o miles de archivos créanme que con un poco de uso de Everything se van a olvidar de navegar por árboles infinitos de carpetas y listas interminables de archivos. Abrir un archivo en Visual Studio se convierte en activar el Everything con un shortcut, escribir unas letras y presionar enter.

Unos pocos tips para su uso:

  • Incluirlo entre los programas de inicio, pesa muy poco e inicia sin que lo notemos.
    Setear HotKey

    La única configuración necesaria: el HotKey.

  • Setear un shortcut, no viene por defecto y es uno de los secretos para su uso, yo uso Windows + S. (Se agrega en: Tools – General – New Windows HotKey Modifier / Key)
  • Excluir las carpetas sobre las que no queremos resultados: (Tools – Exclude / Volumes)
  • Si queremos buscar dentro de una carpeta específica, la escribimos antes del nombre del nombre del archivo seguida de barra invertida. (“MiProyecto\  app.config”)
  • Si trabajan con varios entornos abiertos, asegurensé de tener el foco sobre el que quieren abrir el archivo buscado antes de activar Everything.

¡Extra, Extra!

Everything tiene como extra unas pocas pero poderosas funcionalidades.

Permite exponer un disco o carpetas seleccionadas con Everything actuando como un mini web server, pudiendo hacer busquedas desde otra máquina mediante un browser ¡y bajarse los archivos encontrados!

Podemos exponer parte de nuestros archivos por HTTP. ¡Usar con responsabilidad!

Posee un protocolo propio (ETP: Everything Transfer Protocol) que nos permite setear una instancia Everything como servidor y utilizar otras como clientes de búsqueda, con la misma funcionalidad que usándolo en forma local. Fantástico.

Si quieren hilar fino se puede configurar para usar Regular expressions, permite exportar a un archivo de texto la lista de archivos encontrados, y tiene multiplicidad de opciones que vienen configuradas como para no tener que tocarlas.

Asique ya saben, basta del perro de XP y de la (mucho mejor pero igualmente) no muy efectiva barra de búsqueda de Vista, no tienen nada que hacer al lado de Everything. Después me cuentan.

links: Everything





Sobre base64. Para que usarlo, para que no, y como manejarlo en .net y Asp.Net

6 11 2009

¿Que es base64?

Base64 no es en principio otra cosa mas que un sistema numérico, el cual debido a sus características se emplea en muchos ámbitos de la informática para representar información binaria.

Todos los sistemas de numeración tienen una lista de símbolos que utilizan para representar valores, por ejemplo:

Binario: ‘01′
Decimal: ‘0123456789′
Hexadecimal: ‘0123456789ABCDEF’
y para base64 el conjunto es:
‘ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/’

Como vemos, es un subconjunto de ASCII, y tiene como particularidad que todos sus caracteres son imprimibles, de hecho 64 es la mayor potencia de 2 que permite ser representada por un subconjunto de caracteres ASCII imprimibles, por eso, si pasamos cualquier información a su representación de base64, tenemos la seguridad de que no tendremos problemas al transmitirla, almacenarla o leerla, incluso aunque esta contega los más remotos caracteres unicode, una imagen o un mp3, ya que para convertir algo a base64 trataremos directamente con los bits.

¿Como codificamos en base64?

Veamos (en este ejemplo flagrantemente robado de Wikipedia) como se codifica la palabra “Man”.

Texto de entrada M a n
ASCII 77 97 110
Bits 0 1 0 0 1 1 0 1 0 1 1 0 0 0 0 1 0 1 1 0 1 1 1 0
Índice 19 22 5 46
Resultado en Base64 T W F u

Tenemos que la M es en ASCII el caracter 77 (01001101 en binario), y como el primer paso para nuestra tarea es convertir la información de origen en bits (más sobre esto en un momento), transformamos toda la cadena en una secuencia de unos y ceros, los cuales tomamos en grupos de a seis.

El primero de estos grupos (010011) representa 19, y si tomamos la posición 19 en la lista de símbolos de base64 (empezando a contar desde cero, claro) obtenemos la T, y continuando con este algoritmo llegamos al resultado “TWFu”.

Caracteres de Relleno

base64 se codifica tomando los bytes de tres como entrada en cuatro sextetos, o sea de a 24 bits. Cuando esta cantidad no coincide, completamos con el caracter de relleno “=” los sextetos restantes.

Por ejemplo, si hubiéramos codificado solamente el texto “Ma”, veríamos que no coincide la cantidad de octetos con la de sextetos, luego de “TW” me quedaría 0001, por lo que completo con ocho ceros y obtengo “TWE”. El sexteto vacío que queda se completa con el caraceter “=” quedando como resultado: “TWE=”

Hasta aquí como se codifica. De la misma manera, podría codificar un archivo ejecutable, una imagen, archivos de sonido o simplemente como en este caso, Texto Plano.

Ooops, acabo de decir “texto plano”, pero como bien dice Joel Spolsky (parafraseando a famoso economista que parafraseaba a conocido escritor de ciencia ficción): “No existe tal cosa como el texto plano”. Y acá es donde entra a fastidiarnos la palabra “Encoding”.

Encoding

La representación lógica de una cadena de texto, como puede serlo un String de .net, se puede convertir a bits de muchas maneras. No es lo mismo la representación binaria de texto en ASCII que en Unicode, al respecto recomiendo encarecidamente leer el citado post de Joel Spolsky, pero lo que quiero destacar ahora a los efectos prácticos de comprender el código es que, cuando trabamos con texto, este es para nosotros la representación lógica de la información, la cual se perderá al momento de trabajar directramente con los bits, y como la conversión de esta información lógica a bits puede realizarse de muchas maneras distintas, debemos tener control de este paso al momento de codificar y decodificar texto en y desde base64.

Base64 en .net

Para convertir texto a bytes .net nos ofrece la clase abstracta Encoding y sus derivados concretos, en el ejemplo utilizaremos UTF8Encoding. Si no necesitamos usarlas en grandes iteraciones, podemos obtener instancias de estas clases desde las propiedades estáticas que nos da la clase Encoding, (.UTF8, .UTF7, .ASCII).

Y para obtener los bytes utilizamos el método GetBytes, dado un string “s”:

Encoding.UTF8.GetBytes(s)

Estos bytes se los pasamos a Convert.ToBase64String:

String b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(s));

y a la inversa, mediante el método GetString de la misma clase de Encoding que hayamos utilizado en la conversión:

String s = Encoding.UTF8.GetString(Convert.FromBase64String(b64));

URLTokenEncode, base64 en ASP.NET

Utilizar parámetros codificados en base64 en un query string nos aporta la seguridad de que no tendremos problemas con los caracteres reservados por HTML, así como la ofuscación de la información ante los usuarios, pero nos encontramos ante el problema de los símbolos “+”, “/” y “=” que tienen un significado dentro de una URI.

Ante esta limitación .net nos da una alternativa a la codificación clásica mediante el método estático UrlTokenEncode de la clase HttpServerUtility, en System.Web:

String b64=HttpServerUtility.UrlTokenEncode(Encoding.UTF8.GetBytes(s));
String s=Encoding.UTF8.GetString(HttpServerUtility.UrlTokenDecode(b64));

Con este método, los caracteres “+” y “/” son reemplazados por “-“ y “_” respectivamente y los simbolos “=” de complemento se reemplazan por un dígito numérico indicando la cantidad de los mismos, de esta manera la cadena “ûï¾x%óaxz” se códifica (en UTF8) como: “w7vDr8K+eCXDs2F4eg==” según el método estándar y con UrlTokenEncode “w7vDr8K-eCXDs2F4eg2”, donde el “2” final representa los “==” al final del resultado estándar y el “-“ reemplaza al “+”.

De esta manera obtenemos una cadena totalmente segura para utilizar en el ámbito web.

Algunos hechos sobre base64

  • Base64 no es un método de encriptación. Si bien ofusca la información ante la simple vista no hace falta mucho conocimiento ni pericia para recuperar la información original.
  • Base64 “pesa” un 33% más que la información original. Esta ralación está dada por la relación 8 a 6 bits por caracter, (o sea, un caracter de base64 representa menos información que un caracter normal).
  • Aparte del nombrado Issue en su utilización en Web, en otros ámbitos también hay caracteres que se tornan problemáticos, por ejemplo en regular expressions, se suelen reemplazar “+” y “/” por “[“ y “]”. Observesé que la codificación que utiliza UrlTokenEncode no serviría en este caso ya que los “+” tienen significado propio en Regular Expression, por lo que habría que escaparlos.
  • Insisto: No sirve para encriptar información. No utiliza claves ni certificados, la conversión a la información original es prácticamente directa y no está pensado para este fin.
  • Si podemos, en cambio, utilizarlo para guardar o transmitir información previamente cifrada que suele presentar caracteres problemáticos para ser transmitidos o almacenados.




Diferencia entre Find y Filter en jQuery

15 07 2009

jquery_thumbEs muy común confundirse entre estos dos métodos al comenzar con jQuery, pero ambos realizan tareas muy distintas.

Mientras .filter quita, de los elementos contenidos en el objeto $, los que no cumplan con el selector que se le indica, .find busca en los descendientes de los objetos seleccionados.

$("div.nav").find("a") Encuentra los links dentro de cualquier div con la clase css nav, en cambio:

$("a").addClass(".link").filter("[href^=http://]").addClass(".externo"); Agrega la clase “link” a todos los <a> y la clase “externo”, solo a los que poseen una valor para el atributo href que comienza con “http://”, Este es otro ejemplo clásico de jQuery. (Los corchetes se utilizan para buscar en atributos del HTML y el ^ indica el que la cadena buscada debe estar al comienzo del valor del atributo.)

En general .filter se utiliza para trabajar con un subconjunto de otro conjunto sobre el ya realicé una acción, permitiéndonos encadenar varias sentencias en una como en el ejemplo.

En cambio .find es un método muy poderoso que me sirve para navegar dentro del DOM por relaciones “descendiente de” entre elementos. Buscará en los elementos descendientes de los contenidos en $, sin tener en cuenta estos últimos.

Por ejemplo, dado:

<div id="padre">
   <div id="hijo1">
      <div id="nieto1"></div>
   </div>
   <div id="hijo2"></div>
</div>

$("#padre").find("div") Encuentra “hijo1”, “hijo2” y “nieto1”, (y no el div “padre” contenido en el objeto $.)

Este tipo de métodos denominados “transversales”, son los que nos ampliarán el campo de acción cuando excedamos las posibilidades de los selectores.

¡Hasta pronto!





Checkear todos los CheckBox con jQuery

17 06 2009

Uno de los artículos más consultados de este blog explica como configurar una tabla para que al clickear en un Checkbox en la cabecera de una columna, todos los checks de la misma tomen el mismo estado “checked/unchecked”.

Sin embargo, ese código requiere de algunas condiciones:

  • La tabla debe poseer un ID.
  • Cada checkbox debe ser el primer elemento de la celda.
  • Hay que escribir una llamada al evento onclick del checkbox en la cabecera, pasando como parámetro el ID de la tabla, ya sea escribiéndolo o navegando por el DOM para obtenerlo.

Y tiene un grave problema, no funciona en navegadores no-IE.

Cuando empecé a investigar jQuery, quise emular como práctica este código y me encontré con que jQuery nos lleva a plantear una solución mucho mas completa, que no solo resuelve todos los problemas anteriores, si no que nos lleva a escribir en el body la cantidad de javascript: ¡cero!.

Vamos al grano:

Primer paso: Agregar el include del archivo js donde está el código. (aparte del de jQuery por supuesto)

<script src="js/jquery.js" type="text/javascript"></script>
<script src="js/miScript.js" type="text/javascript"></script>

Segundo y último paso: En miScript.js el siguiente código:

$(function() {
    $("table th :checkbox").click(function() {
        $(this).closest("table")
            .find("td:nth-child(" + ($(this).closest("th").prevAll().size() + 1) + ") :checkbox")
            .attr("checked", $(this).attr("checked"));
    });
});

Aparte de esto, solo nos queda asegurarnos de que los checkbox de la cabecera estén en un TH y no en un TD.

Ahora ya no nos importa si usamos un GridView, una tabla estática, ni si posee tags THEAD y TBODY o solo TRs, ni si el checkbox es un control de servidor o simple HTML. Todos los checkboxs que tengamos en la cabecera (TH) de una tabla provocarán al cambiar su estado que cambien todos los de la misma columna.

No se a Uds. pero a mi me parece fantástico que solamente incluyendo un tag para jQuery y otro para un .js con el script (que seguramente ya estoy incorporando a nuestras páginas para utilizar otras funciones), tenga el problema resuelto en todas las páginas sin hacer más que olvidarme del tema.

Respecto al código, voy a usarlo como parte de la continuación de mi artículo anterior de introducción a jQuery para los que quieran entenderlo.

Nota: Debido al uso de la (excelente) función closest el código requiere jQuery 1.3+.

Saludos: Alejandro





Introducción a jQuery

8 05 2009

He estado usando un poco jQuery y, por si después de lo que ya había visto hacía falta algo más, me terminé de convencer de que jQuery es una tremenda ayuda para quienes programamos mucho o poco en JavaScript. Este es el primero de una serie de artículos introductorios los que voy a tratar de mechar con otros con ejemplos reales. Ahí vamos:

¿Que es jQuery?

Una biblioteca javascript (un .js de 19k en su versión de producción) que tiene las siguientes características:

  • Todo lo que hagamos con jQuery es “Browser-Agnostic”, o sea ¡funciona igual en todos los navegadores!
  • Adhiere al concepto de Unobstrusive javascript, o sea, “No metas javascript en tu HTML”, esto abarca los eventos en los tags y absolutamente todo lo que tenga que ver con javascript en el body, así como la mayoría de los <script> en el head, si no todos. Esto entre otras cosas facilita mucho la separación de tareas entre programador-diseñador.
  • Es OpenSource y al mismo tiempo posee cierto soporte de Microsoft, lo que incluye una versión con Intelisense y la inclusión por defecto en el nuevo  ASP.NET MCV.
  • Una comunidad importantísima, una enorme documentación y cantidad de tutoriales. Unas cuantas de estas cosas están en español.
  • Es extensible y posee una cantidad enorme de Plugins desarrollados, donde “enorme” quiere decir que en el sitio oficial hay más de mil.
  • Posee poderosos métodos para trabajar con Ajax.
  • Muchos efectos gráficos incluidos para manipular los objetos de nuestra página.

También presenta tres conceptos que definen la sintaxis de jQuery y gran parte de su funcionalidad:

  1. Utiliza en su sintaxis el concepto de encadenamiento de sentencias, (A.K.A. fluent interfaces o “chaining”), que nos permite realizar una gran cantidad de acciones en una sola sentencia y nos evita declarar infinidad de variables.
  2. Posee un conjunto de selectores muy poderoso con los cuales podemos recorrer el DOM a gusto y piaccere.
  3. Posee una sintaxis para trabajar con eventos que permite no tener que introducir llamadas a los mismos en los tags y en increíblemente flexible.

Estas características son las que veremos a lo largo de estos introducctorios, pero primero empecemos de cero:

Incorporando jQuery a nuestras páginas

En el sitio oficial: www.jquery.com podemos bajar la biblioteca en sus versiones de desarrollo, producción (minified) y visual studio, está última es una versión comentada que permite el intelisense. El sitio también es referencia en cuando a información se refiere.

Una vez incluido el archivos .js en nuestro proyecto, arrastramos el mismo al tag head de nuestra página, lo que genera lo siguiente:

<script src="js/jquery-1.3.2.min.js" type="text/javascript"></script>

para la versión minified 1.3.2, la actual al momento de escribir esto. (No hay que olvidar cambiar la versión de desarrollo por la de producción cuando ya no la necesitamos y no es mala práctica renombrar el archivo a jQuery a secas para facilitar el cambio de versiones.)

El objeto jQuery ($):

Casi toda la funcionalidad de jQuery está contenida en un objeto llamado también jQuery. Este objeto tiene un alias que es el símbolo $, o sea que en cada lugar donde escribimos $ podemos cambiar por jQuery y viceversa. Como el símbolo $ es el estandar vamos a verlo así escrito en este artículo. Veamos la estructura de una típica sentencia jQuery:

$("selector").acción(parámetros);

$(“selector”) captura, a partir del selector indicado, el conjunto del DOM identificado sobre el cual aplicamos la acción, pero el valor de retorno no es directamente un array de objetos, sino que es el propio objeto $ conteniendo los objetos en su interior.

Cuando ejecutamos un método sobre el objeto $ que contiene los elementos que seleccionamos, este aplica dicha acción a cada uno de los objetos recuperados. Un ejemplo concreto:

$("#mensaje").fadeOut(3000);

Llama para el elemento con id=”mensaje” a la función fadeOut, que aumenta su transparencia hasta hacerlo desaparecer al cabo de los tres segundos indicados como parámetro.

La función .ready

Ya tenemos un pequeño ejemplo de código jQuery, pero ¿donde lo ponemos?

Si leímos bien, una de las características de jQuery es que nos permite no introducir llamadas a funciones javascript en los tags HTML, más tarde vamos a ver como atachar eventos a los objetos del DOM, pero hay un método para ejecutar código en la carga de la página y que veremos hasta en la sopa, la función ready.

Normalmente cuando queremos ejecutar una rutina js en la carga de la página, utilizamos el evento onload del body, pero esto tiene la desventaja de hacernos esperar hasta que se carga toda la página, imágenes inclusive. jQuery nos presenta un camino alternativo que es la función ready, está se ejecuta ni bien el navegador levanto el DOM en memoria, ¿y que hace? bueno, lo que nosotros querramos, ya que toma como único parámetro un callback, o sea, una función javascript, veamos la sintaxis:

$(document).ready(
	function() { ... }
);

Como vemos ready es una función del objeto $, el cual en este caso ha sido creado a partir del document. Como parámetro de ready, estamos enviando una función cuyo código será el que se ejecute correctamente. De esta manera si quisieramos cumplir con la inutil misión de que el código del ejemplo anterior se ejecute en primera instancia al cargar la página, tendríamos que escribir lo siguiente:

<script type="text/javascript">
$(document).ready(function() {
   $("#mensaje").fadeOut(3000);
});
</script>

Donde hemos reemplazado únicamente los “…” por nuestra función.

En la mucha de la documentación más vieja se ve esta sintaxis, pero jQuery presenta una abreviación que es la que vamos a usar:

<script type="text/javascript">
    $(function() {
    	$("#mensaje").fadeOut(3000);
    });
</script>

Hasta aquí hemos visto solo la manera de hacer de forma interesante algo muy inútil, y es que en la gran mayoría de documentos introductorios a jQuery verán que se presenta la librería haciendo uso de fadeIn, fadeOut y varias funciones gráficas que son muy impactantes a la vez que sencillas de utilizar. He estado hace unas semanas en una charla de jQuery donde toda la atención la despertaba este tipo de funcionalidad y en el mismo sitio oficial se presenta la librería con un lindo efecto.

Todo eso está genial, pero si Uds, como yo, nacieron sin la parte del cerebro que se utiliza para el diseño, antes de creer que jQuery es un compendio de efectos o de hacks para algunas funcionalidades, sigan leyendo y encontrarán un tesoro de la mano de la manipulación del DOM, los selectores, el encadenamiento de sentencias, la facilidad para trabajar con AJAX, el manejo de eventos y el crossbrowsing real, por sobre otros instrumentos no tan útiles a la hora de programar comportamientos y manejo de datos en la UI.

Saludos.





Atachar procesos automáticamente con macros de Visual Studio

16 07 2008

La manera de atachar un proceso a Visual Studio es clasicamente mediante ‘Debug – Attach to process’ o el más productivo Ctrl + Alt + P, y luego seleccionar el o los procesos que querramos depurar.

En desarrollos medianamente importantes es muy probable que debamos realizar este procedimiento repetidas veces para depurar un proyecto fuera de la interface de usuario, expuesto mediante una capa de servicios, llámese WS, remoting, WCF, etc. Ni hablar del worker process de ASP.NET,  ‘aspnet_wp.exe’. También es común que corramos un proyecto desde fuera del Visual Studio para evitar la compilación, y luego lo atachemos al IDE.

Entonces, si los procesos que atachamos son siempre los mismos, ¿por que no automatizar la tarea?, bueno, la solución que propongo es una macro de Visual Studio que haga este trabajo, atachando con un atajo de teclado todos los procesos que sean necesarios.

Para incluirla en VS, vamos al editor de macros (Alt + F11), agregamos un nuevo módulo y reemplazamos el contenido por el siguiente:

Imports System.Collections.Generic
Imports EnvDTE
Imports EnvDTE80
Imports System.Text

Public Module DebugMacros

    Private Enum EnumAction
        ResultOK
        ReRunWithDisatach
        Abort
    End Enum

    Public Sub AttachProcs()
        Dim ProcessList As New List(Of String)
        ProcessList.Add("InterfaceUsuario.exe")
        ProcessList.Add("MiServiceLayer.exe")
        AttachProcs(False, ProcessList)
    End Sub

    Private Sub AttachProcs(ByVal detachAll As Boolean, ByVal ProcessList As List(Of String))

        If detachAll Then DTE.Debugger.DetachAll()

        Dim isOK As EnumAction = EnumAction.ResultOK
        Dim sbErrorResult As New StringBuilder

        For Each proc As String In ProcessList
            Dim match As Boolean = False
            For i As Integer = 1 To DTE.Debugger.LocalProcesses.Count
                If DTE.Debugger.LocalProcesses.Item(i).Name.EndsWith(proc) Then
                    match = True
                    Exit For
                End If
            Next
            If Not match Then
                sbErrorResult.Append("El proceso '" + proc + "' no se está ejecutando." + vbCrLf)
                isOK = EnumAction.Abort
            End If
        Next

        If isOK = EnumAction.ResultOK Then
            'Desatacho todos los procesos
            Dim dbg2 As EnvDTE80.Debugger2 = DTE.Debugger
            Dim trans As EnvDTE80.Transport = dbg2.Transports.Item("Default")
            Dim dbgeng(1) As EnvDTE80.Engine
            dbgeng(0) = trans.Engines.Item("Managed")
            Dim proc2 As EnvDTE80.Process2
            For Each processName As String In ProcessList
                Try
                    proc2 = dbg2.GetProcesses(trans, ".").Item(processName)
                    proc2.Attach2(dbgeng)
                Catch ex As Exception
                    If ex.Message.EndsWith("8971001E") Then
                        sbErrorResult.Append("Error atachando " + processName + ". " + "El proceso ya se encuentra atachado" + vbCrLf)
                        isOK = EnumAction.ReRunWithDisatach
                    Else
                        sbErrorResult.Append("Error inesperado atachando " + processName + ". " + ex.Message + vbCrLf)
                        isOK = EnumAction.Abort
                    End If
                End Try
            Next
        Else
        End If

        Select Case isOK
            Case EnumAction.Abort
                DTE.Debugger.DetachAll()
                MsgBox(sbErrorResult.ToString + vbCrLf + "No se ha atachado ningún proceso", MsgBoxStyle.OkOnly, "Error al atachar proceso")
            Case EnumAction.ReRunWithDisatach
                If MsgBox("Hay procesos previamente atachados. ¿Desea reatacharlos?", _
                                    MsgBoxStyle.YesNo, "Atachar Proceso") = MsgBoxResult.Yes Then
                    AttachProcs(True, ProcessList)
                    Exit Sub
                End If
            Case EnumAction.ResultOK
                Dim Resultado As New StringBuilder
                For Each s As String In ProcessList
                    Resultado.Append(s + vbCrLf)
                Next
                MsgBox("Se han atachado los siguientes procesos: " + vbCrLf + Resultado.ToString, MsgBoxStyle.OkOnly, "Proceso atachados")
        End Select
    End Sub

End Module

Observar que en AttachProcs() hay una colección con los nombres de los procesos la cual hay que editar de acuerdo a nuestras necesidades.

Ahora solo falta asignarle una combinación de teclas, desde Tools – Options – Keyboard, filtramos por “Macro” o “AtachProcs” y una vez ubicada la macro le asignamos una combinación de teclas.

Ahora solo basta correr los procesos por fuera de Visual Studio y atacharlos con la combinación de teclas.

Si el código encuentra alguno de los procesos atachado, desatacha todos los procesos e intenta nuevamente.

Nota: Este simple código está hecho a partir de la grabadora de macros (Tools – Macros – Record Temporary Macro), recordemos que todo lo que hacemos en VS queda registrado en la grabadora y que casi todo en el entorno es un objeto que se puede manipular.





Depurar sin complicarnos la vida utilizando Debugging Attributes

29 05 2008

Cuando depuramos con Visual Studio es un verdadero fastidio el hecho de tener que entrar a todos los métodos o propiedades que se encuentran como parámetros de una llamada, por ejemplo:

void MiMétodo(DameUnString())
{
   //algo acá que si quiero depurar
}
String DameUnString()
{
   //¡No quiero depurar esto!
   return "Este es tu String!";
}

DebuggerStepThrough y DebuggerHidden

Cuando se trabaja en cualquier sistema medianamente complejo es muy molesto depurar código como el anterior ya que no hay manera en VS de indicar que queremos ir directamente al cuerpo del método que estamos por depurar, sin entrar a cada parámetro.

Lo mismo ocurre cuando presionamos por error F11 en vez de F10 y entramos a un interminable método que nos obliga a poner un break al final (y luego a quitarlo para no parar nuevamente)

Una solución parcial pero util son los atributos DebuggerStepThrough y DebuggerHidden.

Poniendo DebuggerHidden a un método logramos que Visual Studio no pase nunca por el método:

[System.Diagnostics.DebuggerHidden]
String DameUnString(){...}

hará que nunca depuremos el método DameUnString.

En cambio con el más util DebuggerStepThrough, el entorno se detiene en el método si colocamos un break, pero no de otra manera.

Realmente sería más util una tecla para “Avanzar StepThrough”, incluso por ahí he visto una solicitada para su inclusión en VS…pero esta es la solución parcial que tenemos disponible.

DebuggerBrowsable y DebuggerDisplay

Por defecto VS nos permite ver los miembros privados de las clases y estructuras que escribimos al momento de depurarlas. Si bien esto puede ser util, cuando la clase es un contenedor de datos, no es práctico disponer de esta información que está expuesta nuevamente en las propiedades públicas. Ni hablar si nuestra clase va a ser expuesta a terceros.

Por ejemplo si depuramos una clase “Persona” con tres propiedades vemos en la ventana de Watch una molesta duplicación de datos:

El atributo DebuggerBrowsable nos permite especificar que no se deben mostrar en el depurador los métodos, miembros o propiedades decorados con el mismo.

DebuggerDisplay, a su vez, nos permite especificar el texto a mostrar en la columna value de las ventanas de depuración. En la cadena que nos pide podemos especificar entre llaves “{}” código ejecutable, pero la mejor manera de entender esto es ver un ejemplo. Si la clase de la imagen anterior estuviera declarada de la siguiente manera:

[DebuggerDisplay("{this.GetType().ToString(),nq} ({_nombre,nq} {_apellido,nq}, {_edad} años)")]
public struct Persona {...}

(nq significa “No Quotes”, para quitar las comillas de las cadenas)

y los miembros privados estuvieran decorados así:

[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private Int32 _edad;

Entonces lo que veríamos en la ventana de Watch sería:

Mucho más claro si tenemos en cuenta que el tipo persona lo escribiremos una sola vez pero lo depurarán por miles de años.

Hay otros atributos de debugging que permiten desde wrappear en otra clase lo que se va a mostrar al depurar, hasta implementar un viewer customizado para un tipo, como el que Visual Studio trae para el DataSet.

Nota: No olvidar que estos atributos pertenecen al namespace System.Diagnostics, por lo que hay que agregar el correspondiente using/Imports.





Numalet: convertir números a letras en C# y VB

29 11 2007

Indefectiblemente en algún momento necesitamos o vamos a necesitar código para transformar un número a su numeral cardinal, o sea: 44 en “Cuarenta y cuatro”. Este es otro caso en donde buscamos en google “numeros a letras” o algo así esperando una solución hecha. Bueno, aca la mía: Bajar Numalet, Esta es una solución de VS 2005 con una clase en C#, en VB, un form para probar la clase y un sencillo test unitario para comprobar que las clases en ambos lenguajes funcionan igual.

Al final del post está el código de la clase en C# y en VB para copiarlo directamente.

¿por que usar esta clase y no otra? realmente no hay ningún motivo en especial, cualquier código que resuelva este trivial tema esta bien. La clase que presento genera números acorde a la especificación de la RAE, respetando acentos, apócopes, etc.

Por otro lado, todos los algoritmos que vi generan la salida según el uso del país de quien lo programa, y si queremos otra salida debemos copiar el algoritmo o perder la configuración anterior, asique agregué algunas propiedades que permiten configurar la salida con bastante libertad para cada instancia de la clase.

Hechas estas aclaraciones, veamos un ejemplo sencillo:

C#: Numalet.ToCardinal(8,25);

VB: Numalet.ToCardinal(8,25)

retorna:

ocho con 25/100.-

No hace falta instanciar la clase ya que el método ToCardinal es estático, ahora, si queremos una salida distinta (ahora si instanciando la clase), podemos jugar con las siguientes propiedades:

  • CultureInfo: Permite especificar que configuración regional se utiliza para parsear los números en la sobrecarga que toma strings, en nuestro caso nos importa qué separador decimal (punto o coma) reconocerá, por defecto está seteado en la configuración del thread en que corre el proceso, para cambiarlo de manera que reconozca el punto, por caso, el valor de la propiedad podría ser new CultureInfo(“en-US”). El método estático ToCardinal tiene opcionalmente una sobrecarga que acepta un CultureInfo, por lo que en una máquina donde el separador decimal es la coma, podemos traducir un número que tiene punto como separador decimal de la siguiente manera:

Numalet.ToCardinal(“15.2″,CultureInfo(”en-US”));

Lo que retorna: “quince con 20/100.-”.

  • SeparadorDecimalSalida: Es el texto que aparece entre la parte entera y la decimal, por defecto “con”, por ejemplo: 2,28 = Dos con 28/100.-
  • LetraCapital: Por defecto false, permite obtener “quince” o “Quince“.
  • ConvertirDecimales: Por defecto false, permite especificar si los decimales se muestran como número o como texto.
  • MascaraSalidaDecimal: Es la forma en la que se muestran los decimales, utiliza una cadena para dar formato, por defecto es “00/100.-”.

El comportamiento varía según se estén convirtiendo o no los decimales. Para ello he implementado la siguiente lógica: Si el valor de MascaraSalidaDecimal comienza con ‘#’ o ‘0′, se interpreta como una máscara ‘numérica’ y se utiliza como máscara para los decimales, por ejemplo el caso por defecto (00/100.-).

Si en cambio la cadena empieza por cualquier otro caracter, se toma como literal y se agrega al final del resultado. Por ejemplo: MascaraSalidaDecimal = “centavos” => 45,28 = cuarenta y cinco con 28 centavos.

Hay que notar que todos los caracteres posteriores a los ‘0′ o ‘#’ se toman como literales, internamente se entrecomillan con (‘ ) comilla simple, por lo tanto, si queremos que aparezca una comilla simple en la máscara, debemos escribirla doble, por ej. si MascaraSalidaDecimal = litros de Jack Daniel’’s, (y SeparadorDecimalSalida = ‘coma’ ) entonces 1,2 = uno coma dos litros de Jack Daniel’s.

  • Decimales: Indica la posición decimal que se utilizará para redondear los decimales al convertirlos a enteros. Recordemos que al pasar un número a su nombre, los decimales se expresan como un entero (Ej. 0,21 = ‘cero con 21 centésimas’, el 21 es entero), pero en un número como 8,54613654 ¿cual será el entero que represente los decimales?. Bueno, es lo que se especifica con esta propiedad, por ejemplo si Decimales=2 (valor por defecto) entonces 8,54613654 = ‘ocho con 55/100.-’

Esta propiedad cambia automáticamente al variar MascaraSalidaDecimal. La cantidad de caracteres ‘0′ o ‘#’ hasta el primer caracter distinto se toma como valor para Decimales. O sea, si MascaraSalidaDecimal = “##0 sobre mil”, entonces Decimales = 3, por lo que 456,45667 se convierte en ‘cuatrocientos cincuenta y seis con 457 sobre mil’

Tener en cuenta que el funcionamiento de MascaraSalidaDecimal es un poco diferente que las cadenas de formato del framework, por ejemplo, es lo mismo en el framework hacer #/100 que ###/100, pero en nuestro caso sirve para establecer que queremos redondear en tres decimales.

Sea cual sea el valor de MascaraSalidaDecimal podemos cambiar la propiedad Decimales con posterioridad y a nuestro gusto.

  • ApocoparUnoParteDecimal: En castellano, cuando los números cuantifican un sustantivo masculino, se apocopa (se recorta el final de la palabra) de las decenas mayores a veinte terminadas en uno, ¿que? que en vez de poner “treinta y uno elefantes” ponemos “treinta y un elefantes”. Esta propiedad se puede setear individualmente pero también cambia junto con el valor de ConvertirDecimales ya que si queremos convertir decimales a texto, lo normal es apocopar la palabra uno, por ejemplo veintiún centavos. Tener en cuenta que se utiliza solo cuando ConvertirDecimales es verdadero.
  • ApocoparUnoParteEntera: Lo mismo que la anterior pero para las unidades de nuestro número. Esta propiedad cambia junto con SeparadorDecimalSalida. Si seteamos esta última a, por ejemplo, “pesos con”, entonces se deduce que estoy cualificando algo (pesos) y la segunda palabra (con) se toma como conjunción (adverbio conjuntivo para los puristas creo), por lo que ApocoparUnoParteEntera pasa a ser true. ¿No entendiste? somos dos, veamos algunos ejemplos:
    • si SeparadorDecimalSalida = ‘con’ entonces ApocoparUnoParteEntera es false y 31,2 = ‘treinta y uno con 20/100.-’
    • si SeparadorDecimalSalida = ‘euros con’ entonces ApocoparUnoParteEntera es true y 31,2 = ‘treinta y un euros con 20/100.-’

    Esta propiedad también se puede cambiar con posterioridad a SeparadorDecimalSalida si no se desea este comportamiento.

El código tiene en cuenta que:

  • Los números 16,21,22,23 y 26 se acentúan.
  • El 21 cuando se apocopa se acentúa (veintiún).
  • Cuando cuantificamos miles, cientos de miles o millones, hay que apocopar la palabra o terminación “uno”, por ejemplo 21 es “veintiuno”, pero 21.000 es “veintiún mil”.
  • Permite convertir números mayores o iguales a cero y menores que un billón. o sea, el mayor número permitido es 999.999.999.999 ¿te alcanza?

No tiene en cuenta el género femenino, por ejemplo no genera “quinientas mujeres”. Se puede tocar el código para tener en cuenta esto, pero hay que tener cuidado porque los millones son siempre masculinos: “quinientos millones quinientas mil mujeres”, y los apocopés no se utilizan en numéros femeninos, por ej. ‘treinta y una montañas’ y no ‘treinta y un montañas’. Si alguien lo utiliza lo agrego.

Rendimiento:

La clase convierte 1.000.000 de números generados al azar en menos de 7 segundos en mi máquina (P4). Casi no hay ventaja en instanciar la clase, conviene hacerlo solo si vamos a variar la configuración.

Algunos ejemplos:

MessageBox.Show(Numalet.ToCardinal("18,25"));
//dieciocho con 25/100.-

//Si tenemos el número en un string con otro separador decimal (por ejemplo punto):
MessageBox.Show(Numalet.ToCardinal("155.38", new CultureInfo("en-US")));
//ciento cincuenta y cinco con 38/100.-

//instanciando la clase podemos generar salidas variadas
Numalet let;
let = new Numalet();
//un porcentaje como he visto en algunos documentos de caracter legal:
let.MascaraSalidaDecimal = "por ciento";
let.SeparadorDecimalSalida = "con";
let.ConvertirDecimales = true;
MessageBox.Show(let.ToCustomCardinal(21.2));
//veintiuno con veinte por ciento

let = null;
let = new Numalet();
//al uso en México (creo):
let.MascaraSalidaDecimal = "00/100 M.N.";
let.SeparadorDecimalSalida = "pesos";
//observar que sin esta propiedad queda "veintiuno pesos" en vez de "veintiún pesos":
let.ApocoparUnoParteEntera = true;
MessageBox.Show("Son: " + let.ToCustomCardinal(1121.24));
//Son: un mil ciento veintiún pesos 24/100 M.N.

//algo más raro
let.MascaraSalidaDecimal = "###0 dracmas";
//###0 quiere hace que se redondee a 4 decimales y no se muestren los ceros a la izquierda,
//en cambio 0000 haría que, aparte de redondear en 4, se muestren los ceros a la izquierda.
let.SeparadorDecimalSalida = "talentos y";
let.LetraCapital = true;
MessageBox.Show(let.ToCustomCardinal(12.085));
//Doce talentos y 850 dracmas

//una variación del anterior redondeando decimales a mano
let.ConvertirDecimales = true;
//redondeando en cuatro decimales
let.Decimales = 4;
let.MascaraSalidaDecimal = "dracmas";
MessageBox.Show(let.ToCustomCardinal(21.50028354));
//Veintiún talentos y cinco mil tres dracmas
let = null;

Para quien quiera un comportamiendo fijo distinto al que tiene la clase, puede cambiar los valores de las constantes en la clase.

Espero que les sirva y saludos.

Clase en C#:

using System;
using System.Text;
using System.Globalization;

/// <summary>
/// Convierte números en su expresión numérica a su numeral cardinal
/// </summary>
public sealed class Numalet
{
    #region Miembros estáticos

    private const int UNI = 0, DIECI = 1, DECENA = 2, CENTENA = 3;
    private static string[,] _matriz = new string[CENTENA + 1, 10]
        {
            {null," uno", " dos", " tres", " cuatro", " cinco", " seis", " siete", " ocho", " nueve"},
            {" diez"," once"," doce"," trece"," catorce"," quince"," dieciséis"," diecisiete"," dieciocho"," diecinueve"},
            {null,null,null," treinta"," cuarenta"," cincuenta"," sesenta"," setenta"," ochenta"," noventa"},
            {null,null,null,null,null," quinientos",null," setecientos",null," novecientos"}
        };

    private const Char sub = (Char)26;
    //Cambiar acá si se quiere otro comportamiento en los métodos de clase
    public const String SeparadorDecimalSalidaDefault = "con";
    public const String MascaraSalidaDecimalDefault = "00'/100.-'";
    public const Int32 DecimalesDefault = 2;
    public const Boolean LetraCapitalDefault = false;
    public const Boolean ConvertirDecimalesDefault = false;
    public const Boolean ApocoparUnoParteEnteraDefault = false;
    public const Boolean ApocoparUnoParteDecimalDefault = false;

    #endregion

    #region Propiedades

    private Int32 _decimales = DecimalesDefault;
    private CultureInfo _cultureInfo = CultureInfo.CurrentCulture;
    private String _separadorDecimalSalida = SeparadorDecimalSalidaDefault;
    private Int32 _posiciones = DecimalesDefault;
    private String _mascaraSalidaDecimal, _mascaraSalidaDecimalInterna = MascaraSalidaDecimalDefault;
    private Boolean _esMascaraNumerica = true;
    private Boolean _letraCapital = LetraCapitalDefault;
    private Boolean _convertirDecimales = ConvertirDecimalesDefault;
    private Boolean _apocoparUnoParteEntera = false;
    private Boolean _apocoparUnoParteDecimal;

    /// <summary>
    /// Indica la cantidad de decimales que se pasarán a entero para la conversión
    /// </summary>
    /// <remarks>Esta propiedad cambia al cambiar MascaraDecimal por un valor que empieze con '0'</remarks>
    public Int32 Decimales
    {
        get { return _decimales; }
        set
        {
            if (value > 10) throw new ArgumentException(value.ToString() + " excede el número máximo de decimales admitidos, solo se admiten hasta 10.");
            _decimales = value;
        }
    }

    /// <summary>
    /// Objeto CultureInfo utilizado para convertir las cadenas de entrada en números
    /// </summary>
    public CultureInfo CultureInfo
    {
        get { return _cultureInfo; }
        set { _cultureInfo = value; }
    }

    /// <summary>
    /// Indica la cadena a intercalar entre la parte entera y la decimal del número
    /// </summary>
    public String SeparadorDecimalSalida
    {
        get { return _separadorDecimalSalida; }
        set
        {
            _separadorDecimalSalida = value;
            //Si el separador decimal es compuesto, infiero que estoy cuantificando algo,
            //por lo que apocopo el "uno" convirtiéndolo en "un"
            if (value.Trim().IndexOf(" ") > 0)
                _apocoparUnoParteEntera = true;
            else _apocoparUnoParteEntera = false;
        }
    }

    /// <summary>
    /// Indica el formato que se le dara a la parte decimal del número
    /// </summary>
    public String MascaraSalidaDecimal
    {
        get
        {
            if (!String.IsNullOrEmpty(_mascaraSalidaDecimal))
                return _mascaraSalidaDecimal;
            else return "";
        }
        set
        {
            //determino la cantidad de cifras a redondear a partir de la cantidad de '0' o '#' 
            //que haya al principio de la cadena, y también si es una máscara numérica
            int i = 0;
            while (i < value.Length
                && (value[i] == '0')
                    | value[i] == '#')
                i++;
            _posiciones = i;
            if (i > 0)
            {
                _decimales = i;
                _esMascaraNumerica = true;
            }
            else _esMascaraNumerica = false;
            _mascaraSalidaDecimal = value;
            if (_esMascaraNumerica)
                _mascaraSalidaDecimalInterna = value.Substring(0, _posiciones) + "'"
                    + value.Substring(_posiciones)
                    .Replace("''", sub.ToString())
                    .Replace("'", String.Empty)
                    .Replace(sub.ToString(), "'") + "'";
            else
                _mascaraSalidaDecimalInterna = value
                    .Replace("''", sub.ToString())
                    .Replace("'", String.Empty)
                    .Replace(sub.ToString(), "'");
        }
    }

    /// <summary>
    /// Indica si la primera letra del resultado debe estár en mayúscula
    /// </summary>
    public Boolean LetraCapital
    {
        get { return _letraCapital; }
        set { _letraCapital = value; }
    }

    /// <summary>
    /// Indica si se deben convertir los decimales a su expresión nominal
    /// </summary>
    public Boolean ConvertirDecimales
    {
        get { return _convertirDecimales; }
        set
        {
            _convertirDecimales = value;
            _apocoparUnoParteDecimal = value;
            if (value)
            {// Si la máscara es la default, la borro
                if (_mascaraSalidaDecimal == MascaraSalidaDecimalDefault)
                    MascaraSalidaDecimal = "";
            }
            else if (String.IsNullOrEmpty(_mascaraSalidaDecimal))
                //Si no hay máscara dejo la default
                MascaraSalidaDecimal = MascaraSalidaDecimalDefault;
        }
    }

    /// <summary>
    /// Indica si de debe cambiar "uno" por "un" en las unidades.
    /// </summary>
    public Boolean ApocoparUnoParteEntera
    {
        get { return _apocoparUnoParteEntera; }
        set { _apocoparUnoParteEntera = value; }
    }

    /// <summary>
    /// Determina si se debe apococopar el "uno" en la parte decimal
    /// </summary>
    /// <remarks>El valor de esta propiedad cambia al setear ConvertirDecimales</remarks>
    public Boolean ApocoparUnoParteDecimal
    {
        get { return _apocoparUnoParteDecimal; }
        set { _apocoparUnoParteDecimal = value; }
    }

    #endregion

    #region Constructores

    public Numalet()
    {
        MascaraSalidaDecimal = MascaraSalidaDecimalDefault;
        SeparadorDecimalSalida = SeparadorDecimalSalidaDefault;
        LetraCapital = LetraCapitalDefault;
        ConvertirDecimales = _convertirDecimales;
    }

    public Numalet(Boolean ConvertirDecimales, String MascaraSalidaDecimal, String SeparadorDecimalSalida, Boolean LetraCapital)
    {
        if (!String.IsNullOrEmpty(MascaraSalidaDecimal))
            this.MascaraSalidaDecimal = MascaraSalidaDecimal;
        if (!String.IsNullOrEmpty(SeparadorDecimalSalida))
            _separadorDecimalSalida = SeparadorDecimalSalida;
        _letraCapital = LetraCapital;
        _convertirDecimales = ConvertirDecimales;
    }
    #endregion

    #region Conversores de instancia

    public String ToCustomCardinal(Double Numero)
    { return Convertir((Decimal)Numero, _decimales, _separadorDecimalSalida, _mascaraSalidaDecimalInterna, _esMascaraNumerica, _letraCapital, _convertirDecimales, _apocoparUnoParteEntera, _apocoparUnoParteDecimal); }

    public String ToCustomCardinal(String Numero)
    {
        Double dNumero;
        if (Double.TryParse(Numero, NumberStyles.Float, _cultureInfo, out dNumero))
            return ToCustomCardinal(dNumero);
        else throw new ArgumentException("'" + Numero + "' no es un número válido.");
    }

    public String ToCustomCardinal(Decimal Numero)
    { return ToCardinal((Numero)); }

    public String ToCustomCardinal(Int32 Numero)
    { return Convertir((Decimal)Numero, 0, _separadorDecimalSalida, _mascaraSalidaDecimalInterna, _esMascaraNumerica, _letraCapital, _convertirDecimales, _apocoparUnoParteEntera, false); }

    #endregion

    #region Conversores estáticos

    public static String ToCardinal(Int32 Numero)
    {
        return Convertir((Decimal)Numero, 0, null, null, true, LetraCapitalDefault, ConvertirDecimalesDefault, ApocoparUnoParteEnteraDefault, ApocoparUnoParteDecimalDefault);
    }

    public static String ToCardinal(Double Numero)
    {
        return ToCardinal((Decimal)Numero);
    }

    public static String ToCardinal(String Numero, CultureInfo ReferenciaCultural)
    {
        Double dNumero;
        if (Double.TryParse(Numero, NumberStyles.Float, ReferenciaCultural, out dNumero))
            return ToCardinal(dNumero);
        else throw new ArgumentException("'" + Numero + "' no es un número válido.");
    }

    public static String ToCardinal(String Numero)
    {
        return Numalet.ToCardinal(Numero, CultureInfo.CurrentCulture);
    }

    public static String ToCardinal(Decimal Numero)
    {
        return Convertir(Numero, DecimalesDefault, SeparadorDecimalSalidaDefault, MascaraSalidaDecimalDefault, true, LetraCapitalDefault, ConvertirDecimalesDefault, ApocoparUnoParteEnteraDefault, ApocoparUnoParteDecimalDefault);
    }

    #endregion

    private static String Convertir(Decimal Numero, Int32 Decimales, String SeparadorDecimalSalida, String MascaraSalidaDecimal, Boolean EsMascaraNumerica, Boolean LetraCapital, Boolean ConvertirDecimales, Boolean ApocoparUnoParteEntera, Boolean ApocoparUnoParteDecimal)
    {
        Int64 Num;
        Int32 terna, centenaTerna, decenaTerna, unidadTerna, iTerna;
        String cadTerna;
        StringBuilder Resultado = new StringBuilder();

        Num = (Int64)Math.Abs(Numero);

        if (Num >= 1000000000000 || Num < 0) throw new ArgumentException("El número '" + Numero.ToString() + "' excedió los límites del conversor: [0;1.000.000.000.000)");
        if (Num == 0)
            Resultado.Append(" cero");
        else
        {
            iTerna = 0;
            while (Num > 0)
            {
                iTerna++;
                cadTerna = String.Empty;
                terna = (Int32)(Num % 1000);

                centenaTerna = (Int32)(terna / 100);
                decenaTerna = terna % 100;
                unidadTerna = terna % 10;

                if ((decenaTerna > 0) && (decenaTerna < 10))
                    cadTerna = _matriz[UNI, unidadTerna] + cadTerna;
                else if ((decenaTerna >= 10) && (decenaTerna < 20))
                    cadTerna = cadTerna + _matriz[DIECI, unidadTerna];
                else if (decenaTerna == 20)
                    cadTerna = cadTerna + " veinte";
                else if ((decenaTerna > 20) && (decenaTerna < 30))
                    cadTerna = " veinti" + _matriz[UNI, unidadTerna].Substring(1);
                else if ((decenaTerna >= 30) && (decenaTerna < 100))
                    if (unidadTerna != 0)
                        cadTerna = _matriz[DECENA, (Int32)(decenaTerna / 10)] + " y" + _matriz[UNI, unidadTerna] + cadTerna;
                    else
                        cadTerna += _matriz[DECENA, (Int32)(decenaTerna / 10)];

                switch (centenaTerna)
                {
                    case 1:
                        if (decenaTerna > 0) cadTerna = " ciento" + cadTerna;
                        else cadTerna = " cien" + cadTerna;
                        break;
                    case 5:
                    case 7:
                    case 9:
                        cadTerna = _matriz[CENTENA, (Int32)(terna / 100)] + cadTerna;
                        break;
                    default:
                        if ((Int32)(terna / 100) > 1) cadTerna = _matriz[UNI, (Int32)(terna / 100)] + "cientos" + cadTerna;
                        break;
                }
                //Reemplazo el 'uno' por 'un' si no es en las únidades o si se solicító apocopar
                if ((iTerna > 1 | ApocoparUnoParteEntera) && decenaTerna == 21)
                    cadTerna = cadTerna.Replace("veintiuno", "veintiún");
                else if ((iTerna > 1 | ApocoparUnoParteEntera) && unidadTerna == 1 && decenaTerna != 11)
                    cadTerna = cadTerna.Substring(0, cadTerna.Length - 1);
                //Acentúo 'veintidós', 'veintitrés' y 'veintiséis'
                else if (decenaTerna == 22) cadTerna = cadTerna.Replace("veintidos", "veintidós");
                else if (decenaTerna == 23) cadTerna = cadTerna.Replace("veintitres", "veintitrés");
                else if (decenaTerna == 26) cadTerna = cadTerna.Replace("veintiseis", "veintiséis");

                //Completo miles y millones
                switch (iTerna)
                {
                    case 3:
                        if (Numero < 2000000) cadTerna += " millón";
                        else cadTerna += " millones";
                        break;
                    case 2:
                    case 4:
                        if (terna > 0) cadTerna += " mil";
                        break;
                }
                Resultado.Insert(0, cadTerna);
                Num = (Int32)(Num / 1000);
            } //while
        }

        //Se agregan los decimales si corresponde
        if (Decimales > 0)
        {
            Resultado.Append(" " + SeparadorDecimalSalida + " ");
            Int32 EnteroDecimal = (Int32)Math.Round((Double)(Numero - (Int64)Numero) * Math.Pow(10, Decimales), 0);
            if (ConvertirDecimales)
            {
                Boolean esMascaraDecimalDefault = MascaraSalidaDecimal == MascaraSalidaDecimalDefault;
                Resultado.Append(Convertir((Decimal)EnteroDecimal, 0, null, null, EsMascaraNumerica, false, false, (ApocoparUnoParteDecimal && !EsMascaraNumerica/*&& !esMascaraDecimalDefault*/), false) + " "
                    + (EsMascaraNumerica ? "" : MascaraSalidaDecimal));
            }
            else
                if (EsMascaraNumerica) Resultado.Append(EnteroDecimal.ToString(MascaraSalidaDecimal));
                else Resultado.Append(EnteroDecimal.ToString() + " " + MascaraSalidaDecimal);
        }
        //Se pone la primer letra en mayúscula si corresponde y se retorna el resultado
        if (LetraCapital)
            return Resultado[1].ToString().ToUpper() + Resultado.ToString(2, Resultado.Length - 2);
        else
            return Resultado.ToString().Substring(1);
    }
}

Clase en VB:

Imports System
Imports System.Text
Imports System.Globalization

''' <summary>
''' Convierte números en su expresión numérica a su numeral cardinal
''' </summary>
Public NotInheritable Class Numalet

#Region "Miembros estáticos"

    Private Const UNI As Integer = 0, DIECI As Integer = 1, DECENA As Integer = 2, CENTENA As Integer = 3
    Private Shared _matriz As String(,) = New String(CENTENA, 9) { _
        {Nothing, " uno", " dos", " tres", " cuatro", " cinco", " seis", " siete", " ocho", " nueve"}, _
        {" diez", " once", " doce", " trece", " catorce", " quince", " dieciséis", " diecisiete", " dieciocho", " diecinueve"}, _
        {Nothing, Nothing, Nothing, " treinta", " cuarenta", " cincuenta", " sesenta", " setenta", " ochenta", " noventa"}, _
        {Nothing, Nothing, Nothing, Nothing, Nothing, " quinientos", Nothing, " setecientos", Nothing, " novecientos"}}
    Private Const [sub] As Char = CChar(ChrW(26))
    'Cambiar acá si se quiere otro comportamiento en los métodos de clase
    Public Const SeparadorDecimalSalidaDefault As String = "con"
    Public Const MascaraSalidaDecimalDefault As String = "00'/100.-'"
    Public Const DecimalesDefault As Int32 = 2
    Public Const LetraCapitalDefault As Boolean = False
    Public Const ConvertirDecimalesDefault As Boolean = False
    Public Const ApocoparUnoParteEnteraDefault As Boolean = False
    Public Const ApocoparUnoParteDecimalDefault As Boolean = False

#End Region

#Region "Propiedades"

    Private _decimales As Int32 = DecimalesDefault
    Private _cultureInfo As CultureInfo = Globalization.CultureInfo.CurrentCulture
    Private _separadorDecimalSalida As String = SeparadorDecimalSalidaDefault
    Private _posiciones As Int32 = DecimalesDefault
    Private _mascaraSalidaDecimal As String, _mascaraSalidaDecimalInterna As String = MascaraSalidaDecimalDefault
    Private _esMascaraNumerica As Boolean = True
    Private _letraCapital As Boolean = LetraCapitalDefault
    Private _convertirDecimales As Boolean = ConvertirDecimalesDefault
    Private _apocoparUnoParteEntera As Boolean = False
    Private _apocoparUnoParteDecimal As Boolean

    ''' <summary>
    ''' Indica la cantidad de decimales que se pasarán a entero para la conversión
    ''' </summary>
    ''' <remarks>Esta propiedad cambia al cambiar MascaraDecimal por un valor que empieze con '0'</remarks>
    Public Property Decimales() As Int32
        Get
            Return _decimales
        End Get
        Set(ByVal value As Int32)
            If value > 10 Then
                Throw New ArgumentException(value.ToString() + " excede el número máximo de decimales admitidos, solo se admiten hasta 10.")
            End If
            _decimales = value
        End Set
    End Property

    ''' <summary>
    ''' Objeto CultureInfo utilizado para convertir las cadenas de entrada en números
    ''' </summary>
    Public Property CultureInfo() As CultureInfo
        Get
            Return _cultureInfo
        End Get
        Set(ByVal value As CultureInfo)
            _cultureInfo = value
        End Set
    End Property

    ''' <summary>
    ''' Indica la cadena a intercalar entre la parte entera y la decimal del número
    ''' </summary>
    Public Property SeparadorDecimalSalida() As String
        Get
            Return _separadorDecimalSalida
        End Get
        Set(ByVal value As String)
            _separadorDecimalSalida = value
            'Si el separador decimal es compuesto, infiero que estoy cuantificando algo,
            'por lo que apocopo el "uno" convirtiéndolo en "un"
            If value.Trim().IndexOf(" ") > 0 Then
                _apocoparUnoParteEntera = True
            Else
                _apocoparUnoParteEntera = False
            End If
        End Set
    End Property

    ''' <summary>
    ''' Indica el formato que se le dara a la parte decimal del número
    ''' </summary>
    Public Property MascaraSalidaDecimal() As String
        Get
            If Not [String].IsNullOrEmpty(_mascaraSalidaDecimal) Then
                Return _mascaraSalidaDecimal
            Else
                Return ""
            End If
        End Get
        Set(ByVal value As String)
            'determino la cantidad de cifras a redondear a partir de la cantidad de '0' o ''
            'que haya al principio de la cadena, y también si es una máscara numérica
            Dim i As Integer = 0
            While i < value.Length AndAlso (value(i) = "0"c OrElse value(i) = "#")
                i += 1
            End While
            _posiciones = i
            If i > 0 Then
                _decimales = i
                _esMascaraNumerica = True
            Else
                _esMascaraNumerica = False
            End If
            _mascaraSalidaDecimal = value
            If _esMascaraNumerica Then
                _mascaraSalidaDecimalInterna = value.Substring(0, _posiciones) + "'" + value.Substring(_posiciones).Replace("''", [sub].ToString()).Replace("'", [String].Empty).Replace([sub].ToString(), "'") + "'"
            Else
                _mascaraSalidaDecimalInterna = value.Replace("''", [sub].ToString()).Replace("'", [String].Empty).Replace([sub].ToString(), "'")
            End If
        End Set
    End Property

    ''' <summary>
    ''' Indica si la primera letra del resultado debe estár en mayúscula
    ''' </summary>
    Public Property LetraCapital() As Boolean
        Get
            Return _letraCapital
        End Get
        Set(ByVal value As Boolean)
            _letraCapital = value
        End Set
    End Property

    ''' <summary>
    ''' Indica si se deben convertir los decimales a su expresión nominal
    ''' </summary>
    Public Property ConvertirDecimales() As Boolean
        Get
            Return _convertirDecimales
        End Get
        Set(ByVal value As Boolean)
            _convertirDecimales = value
            _apocoparUnoParteDecimal = value
            If value Then
                ' Si la máscara es la default, la borro
                If _mascaraSalidaDecimal = MascaraSalidaDecimalDefault Then
                    MascaraSalidaDecimal = ""
                End If
            ElseIf [String].IsNullOrEmpty(_mascaraSalidaDecimal) Then
                MascaraSalidaDecimal = MascaraSalidaDecimalDefault
                'Si no hay máscara dejo la default
            End If
        End Set
    End Property

    ''' <summary>
    ''' Indica si de debe cambiar "uno" por "un" en las unidades.
    ''' </summary>
    Public Property ApocoparUnoParteEntera() As Boolean
        Get
            Return _apocoparUnoParteEntera
        End Get
        Set(ByVal value As Boolean)
            _apocoparUnoParteEntera = value
        End Set
    End Property

    ''' <summary>
    ''' Determina si se debe apococopar el "uno" en la parte decimal
    ''' </summary>
    ''' <remarks>El valor de esta propiedad cambia al setear ConvertirDecimales</remarks>
    Public Property ApocoparUnoParteDecimal() As Boolean
        Get
            Return _apocoparUnoParteDecimal
        End Get
        Set(ByVal value As Boolean)
            _apocoparUnoParteDecimal = value
        End Set
    End Property

#End Region

#Region "Constructores"

    Public Sub New()
        MascaraSalidaDecimal = MascaraSalidaDecimalDefault
        SeparadorDecimalSalida = SeparadorDecimalSalidaDefault
        LetraCapital = LetraCapitalDefault
        ConvertirDecimales = _convertirDecimales
    End Sub

    Public Sub New(ByVal ConvertirDecimales As Boolean, ByVal MascaraSalidaDecimal As String, ByVal SeparadorDecimalSalida As String, ByVal LetraCapital As Boolean)
        If Not [String].IsNullOrEmpty(MascaraSalidaDecimal) Then
            Me.MascaraSalidaDecimal = MascaraSalidaDecimal
        End If
        If Not [String].IsNullOrEmpty(SeparadorDecimalSalida) Then
            _separadorDecimalSalida = SeparadorDecimalSalida
        End If
        _letraCapital = LetraCapital
        _convertirDecimales = ConvertirDecimales
    End Sub

#End Region

#Region "Conversores de instancia"

    Public Function ToCustomCardinal(ByVal Numero As Double) As String
        Return Convertir(Convert.ToDecimal(Numero), _decimales, _separadorDecimalSalida, _mascaraSalidaDecimalInterna, _esMascaraNumerica, _letraCapital, _
        _convertirDecimales, _apocoparUnoParteEntera, _apocoparUnoParteDecimal)
    End Function

    Public Function ToCustomCardinal(ByVal Numero As String) As String
        Dim dNumero As Double
        If [Double].TryParse(Numero, NumberStyles.Float, _cultureInfo, dNumero) Then
            Return ToCustomCardinal(dNumero)
        Else
            Throw New ArgumentException("'" + Numero + "' no es un número válido.")
        End If
    End Function

    Public Function ToCustomCardinal(ByVal Numero As Decimal) As String
        Return ToCardinal(Numero)
    End Function

    Public Function ToCustomCardinal(ByVal Numero As Int32) As String
        Return Convertir(Convert.ToDecimal(Numero), 0, _separadorDecimalSalida, _mascaraSalidaDecimalInterna, _esMascaraNumerica, _letraCapital, _
        _convertirDecimales, _apocoparUnoParteEntera, False)
    End Function

#End Region

#Region "Conversores estáticos"

    Public Shared Function ToCardinal(ByVal Numero As Int32) As String
        Return Convertir(Convert.ToDecimal(Numero), 0, Nothing, Nothing, True, LetraCapitalDefault, _
        ConvertirDecimalesDefault, ApocoparUnoParteEnteraDefault, ApocoparUnoParteDecimalDefault)
    End Function

    Public Shared Function ToCardinal(ByVal Numero As Double) As String
        Return Convertir(Convert.ToDecimal(Numero), DecimalesDefault, SeparadorDecimalSalidaDefault, MascaraSalidaDecimalDefault, True, LetraCapitalDefault, _
        ConvertirDecimalesDefault, ApocoparUnoParteEnteraDefault, ApocoparUnoParteDecimalDefault)
    End Function

    Public Shared Function ToCardinal(ByVal Numero As String, ByVal ReferenciaCultural As CultureInfo) As String
        Dim dNumero As Double
        If [Double].TryParse(Numero, NumberStyles.Float, ReferenciaCultural, dNumero) Then
            Return ToCardinal(dNumero)
        Else
            Throw New ArgumentException("'" + Numero + "' no es un número válido.")
        End If
    End Function

    Public Shared Function ToCardinal(ByVal Numero As String) As String
        Return Numalet.ToCardinal(Numero, CultureInfo.CurrentCulture)
    End Function

    Public Shared Function ToCardinal(ByVal Numero As Decimal) As String
        Return ToCardinal(Convert.ToDouble(Numero))
    End Function

#End Region

    Private Shared Function Convertir(ByVal Numero As Decimal, ByVal Decimales As Int32, ByVal SeparadorDecimalSalida As String, ByVal MascaraSalidaDecimal As String, ByVal EsMascaraNumerica As Boolean, ByVal LetraCapital As Boolean, _
    ByVal ConvertirDecimales As Boolean, ByVal ApocoparUnoParteEntera As Boolean, ByVal ApocoparUnoParteDecimal As Boolean) As String
        Dim Num As Int64
        Dim terna As Int32, centenaTerna As Int32, decenaTerna As Int32, unidadTerna As Int32, iTerna As Int32
        Dim cadTerna As String
        Dim Resultado As New StringBuilder()

        Num = Math.Floor(Math.Abs(Numero))

        If Num >= 1000000000001 OrElse Num < 0 Then
            Throw New ArgumentException("El número '" + Numero.ToString() + "' excedió los límites del conversor: [0;1.000.000.000.001]")
        End If
        If Num = 0 Then
            Resultado.Append(" cero")
        Else
            iTerna = 0

            Do Until Num = 0

                iTerna += 1
                cadTerna = String.Empty
                terna = Num Mod 1000

                centenaTerna = Int(terna / 100)
                decenaTerna = terna - centenaTerna * 100 'Decena junto con la unidad
                unidadTerna = (decenaTerna - Math.Floor(decenaTerna / 10) * 10)

                Select Case decenaTerna
                    Case 1 To 9
                        cadTerna = _matriz(UNI, unidadTerna) + cadTerna
                    Case 10 To 19
                        cadTerna = cadTerna + _matriz(DIECI, unidadTerna)
                    Case 20
                        cadTerna = cadTerna + " veinte"
                    Case 21 To 29
                        cadTerna = " veinti" + _matriz(UNI, unidadTerna).Substring(1)
                    Case 30 To 99
                        If unidadTerna <> 0 Then
                            cadTerna = _matriz(DECENA, Int(decenaTerna / 10)) + " y" + _matriz(UNI, unidadTerna) + cadTerna
                        Else
                            cadTerna += _matriz(DECENA, Int(decenaTerna / 10))
                        End If
                End Select

                Select Case centenaTerna
                    Case 1
                        If decenaTerna > 0 Then
                            cadTerna = " ciento" + cadTerna
                        Else
                            cadTerna = " cien" + cadTerna
                        End If
                        Exit Select
                    Case 5, 7, 9
                        cadTerna = _matriz(CENTENA, Int(terna / 100)) + cadTerna
                        Exit Select
                    Case Else
                        If Int(terna / 100) > 1 Then
                            cadTerna = _matriz(UNI, Int(terna / 100)) + "cientos" + cadTerna
                        End If
                        Exit Select
                End Select
                'Reemplazo el 'uno' por 'un' si no es en las únidades o si se solicító apocopar
                If (iTerna > 1 OrElse ApocoparUnoParteEntera) AndAlso decenaTerna = 21 Then
                    cadTerna = cadTerna.Replace("veintiuno", "veintiún")
                ElseIf (iTerna > 1 OrElse ApocoparUnoParteEntera) AndAlso unidadTerna = 1 AndAlso decenaTerna <> 11 Then
                    cadTerna = cadTerna.Substring(0, cadTerna.Length - 1)
                    'Acentúo 'veintidós', 'veintitrés' y 'veintiséis'
                ElseIf decenaTerna = 22 Then
                    cadTerna = cadTerna.Replace("veintidos", "veintidós")
                ElseIf decenaTerna = 23 Then
                    cadTerna = cadTerna.Replace("veintitres", "veintitrés")
                ElseIf decenaTerna = 26 Then
                    cadTerna = cadTerna.Replace("veintiseis", "veintiséis")
                End If

                'Completo miles y millones
                Select Case iTerna
                    Case 3
                        If Numero < 2000000 Then
                            cadTerna += " millón"
                        Else
                            cadTerna += " millones"
                        End If
                    Case 2, 4
                        If terna > 0 Then cadTerna += " mil"
                End Select
                Resultado.Insert(0, cadTerna)
                Num = Int(Num / 1000)
            Loop
        End If

        'Se agregan los decimales si corresponde
        If Decimales > 0 Then
            Resultado.Append(" " + SeparadorDecimalSalida + " ")
            Dim EnteroDecimal As Int32 = Int(Math.Round((Numero - Int(Numero)) * Math.Pow(10, Decimales)))
            If ConvertirDecimales Then
                Dim esMascaraDecimalDefault As Boolean = MascaraSalidaDecimal = MascaraSalidaDecimalDefault
                Resultado.Append(Convertir(Convert.ToDecimal(EnteroDecimal), 0, Nothing, Nothing, EsMascaraNumerica, False, _
                False, (ApocoparUnoParteDecimal AndAlso Not EsMascaraNumerica), False) + " " + (IIf(EsMascaraNumerica, "", MascaraSalidaDecimal)))
            ElseIf EsMascaraNumerica Then
                Resultado.Append(EnteroDecimal.ToString(MascaraSalidaDecimal))
            Else
                Resultado.Append(EnteroDecimal.ToString() + " " + MascaraSalidaDecimal)
            End If
        End If
        'Se pone la primer letra en mayúscula si corresponde y se retorna el resultado
        If LetraCapital Then
            Return Resultado(1).ToString().ToUpper() + Resultado.ToString(2, Resultado.Length - 2)
        Else
            Return Resultado.ToString().Substring(1)
        End If
    End Function

End Class




Chequear todos los checkbox de un gridview con javascript

3 11 2007

NOTA 16/06/2009: He publicado como hacer esta misma tarea con jQuery en este artículo, recomiendo utilizar esa alternativa por los motivos que comento en dicho artículo.

Muchas veces queremos chequear o deschequear todos los checkbox de un gridview de una sola vez, puede ser cuando el usuario realiza determinada acción en otro control, por ejemplo un botón para “seleccionar todo”, o que cuando se activa un CheckBox en la cabecera de la columna, cambie el estado de todos los checks al de la cabecera.

Si bien esto se puede hacer con código de servidor, normalmente no queremos que se realice un postback solo para esto, la solución es javascript.

(Nuevamente, si solo te interesa un código js que solucione el problema que acabo de describir, te recomiendo ir directamente a copiar los dos fragmentos de javascript al final del post.)

En su momento utilicé el código que Scott Mitchell publicó en este artículo, pero esta solución, si bien funciona y no genera un postback, requiere código de servidor, ya que en la creación de la página se registra mediante el método RegisterArrayDeclaration un array con los ids que tendrá cada checkbox en el cliente, lo que nos obliga a hacerlo en cada postback o a recargar el estado, ya sea ViewState o Session.

Buscando otra alternativa encontré este otro artículo de Mohammad Azam, el autor de GridViewGuy. Este si es un ejemplo totalmente sobre javascript, pero tiene un gran defecto: opera sobre todos los checkbox de la página, sin importar si están dentro de la grilla o no, ni hablar si tenemos dos grillas.

En este punto decidí escribir mi propia solución, o sea una totalmente en el cliente y que permita operar sobre los checkbox de una grilla en particular y, por que no, sobre una columna en particular, de manera que podamos tener más de una columna con checkbox y cambiar el estado de los checks de una de ellas sin afectar la otra.

Un poco de background que pueden saltarse si les parece:

Como incluir CheckBox en una columna de un gridview

Antes que nada hay que generar una columna con los checkbox. Hay dos maneras de hacer esto como cuenta Scott Mitchell en el artículo nombrado, yo utilizo también un TemplateField (no encontré todavía utilidad para los CheckBoxField), de modo que colocando un checkbox en el ItemTemplate genero uno en cada celda del TemplateField:

<asp:TemplateField HeaderText="chk">
   <ItemTemplate>
      <asp:CheckBox ID="chkColumna1" runat="server" />
   </ItemTemplate>
</asp:TemplateField>

Visualmente (en VisualStudio), agregamos un TemplateField al gridview y seleccionamos EditTemplate del menú contextual. Luego arrastramos un CheckBox a la zona ItemTemplate. Para finalizar: End Template Editing, en el menú contextual.

Como chequear todos CheckBox de un GridView

Esta rutina javascript cambia el estado de todos los checkbox de una grilla sin importar su ubicación:

function ChangeAllChecks(gridViewName,newState)
{
   var tabla = document.getElementById(gridViewName);
   celdas = tabla.cells;
   for(i=0;i<celdas.length-1;i++)
   {
   if (celdas[i].firstChild.type=="checkbox"
   && celdas[i].firstChild.checked != newState)
      {
         celdas[i].firstChild.click();
      }
   }
}

Para llamarla utilizamos ChangeAllChecks(‘GridView1′,true); o false si es el caso

Como chequear una columna específica

Puede ocurrir que en un gridview tengamos más de un TemplateField con CheckBox, y que solo queramos chequear o deschequear una sola columna, para ello hice un nuevo método que toma como paramétro el índice de la columna (empezando por 0). (Actualizado 29/09/2008 según respuesta al comentario de Martín)

function ChangeChecksByColumn(gridViewName, newState, columnIndex){
    var tabla = document.getElementById(gridViewName);
    var columnas = tabla.cells.length / tabla.rows.length;
    celdas = tabla.cells;
    for (i = columnas + columnIndex; i < celdas.length; i += columnas){
        if (celdas[i].firstChild.type == "checkbox"
               && celdas[i].firstChild.checked != newState
            /* && agregar aquí otras condiciones */){
            celdas[i].firstChild.click();
        }
    }
}

Como chequear a partir de un CheckBox en la cabecera

El código anterior no es muy útil porque nos solicita el estado (true=checked/false=unchequed) en el que queremos dejar los checkbox, la solución es agregar un checkbox en la cabecera de la TemplateColumn:

y agregamos una rutina que permita identificar el estado del mismo y la columna en que se encuentra, luego llamamos al método anterior: ChangeChecksByColumn:

function CopyCheckStateByColumn(HeaderCheckBox, gridViewName)
{
    var columnIndex = HeaderCheckBox.parentElement.cellIndex;
    var newState = HeaderCheckBox.checked;
    ChangeChecksByColumn(gridViewName, newState, columnIndex);
}

Para terminar, completamos el evento onclick del checkbox con una llamada a CopyCheckStateByColumn pasando como parámetro el propio checkbox y el nombre del gridview:
(lo que sigue cambiado el 08/11 según la respuesta al comentario de Roger)

<asp:TemplateField HeaderText="chk">
<HeaderTemplate>
    <asp:CheckBox ID="chkHeader" runat="server" onclick="javascript:CopyCheckStateByColumn(this,this.offsetParent.offsetParent.id);"/>
</HeaderTemplate>
<ItemTemplate>
    <asp:CheckBox ID="chkTest" runat="server" />
</ItemTemplate>
</asp:TemplateField>

Una precaución: Tener en cuenta que el código javascript consulta si el contenido es un CheckBox mediante la propiedad firstChild para cada objeto de la colección cells del objeto table, o sea: el contenido de cada TD. Si ponemos otro objeto en el ItemTemplate junto con el CheckBox, podemos tener problemas si nos descuidamos.

Un tip: Para llamar métodos javascript en el evento click de un control Button, se debe utilizar el evento OnClientClick en vez de onclick.

Una desventaja: No funciona en Firefox.

Saludos





Envío de mail con framework 2.0, mi clase con autenticación

30 10 2007

A todos nos llega en algún momento la necesidad de que nuestro sistema envíe mails. Es ahí donde donde toda nuestra pasión por la programación se reduce a google y “ojala que alguién lo tenga resuelto”.

No es difícil encontrar como enviar mails con .Net, pero luego nos encontramos con diferentes requerimientos de formato, seguridad, etc. y como no queremos interiorizarnos en como MS implementó el aburrido protocolo SMTP en el framework, recurrimos nuevamente a google para aburrirnos un rato más. Bueno, he aquí el producto de mi tiempo desperdiciado y algunos comentarios sobre el tema. Si solamente necesitás una clase para enviar mails sin mucha vuelta te recomiendo ir directamente al código y aplicar un “copy-paste”, ya que el código es bastante auto-explicativo y hay un solo método para utilizar.

El namespace utilizado es System.Net.Mail (solo framework 2.0, antes era System.Web.Mail)

Basicamente, lo que hacemos es:

  • Crear un objeto MailMessage
  • Crear y asignar a la propiedad “From” del objeto MailMessage un objeto MailAddress con la dirección de salida
  • Crear y asignar a la propiedad “To” del objeto MailMessage una colección de objetos MailAddress conteniendo las direcciones de los destinatarios
  • Asignar un texto a las propiedades “Subject” y Body” del objeto MailMessage
  • Crear un objeto SmtpClient, definirle la dirección del host y el puerto, e invocar a su método Send pasándole como parámetro nuestro MailMessage

Esa es la manera de enviar un mail simple, con un body de texto plano, sin attachments y por el puerto 25 (default de SMTP), sin embargo, el único método público de mi clase (Enviar) tiene una serie de parámetros opcionales (serán sobrecargas para quien lo quiera pasar a C#) que permiten utilizar otras características:

MailPort permite especificar la utilización de otro puerto para conectarse al host SMTP (por ejemplo 443 para utilizar ssl), altarando la propiedad Port del SmtpClient .

MailIsBodyHtml permite especificar si el texto enviado es Html o texto plano (propiedad IsBodyHtml del objeto MailMessage).

EnableSSL determina si se utiliza (1), o no (0) este protocolo en la conexión con el host (propiedad EnableSsl de SmtpClient).

SSLuser y SSLpass permite enviar al host un user y password para establecer la conexión (propiedad Credentials de SmtpClient).

En el código no manejo attachments, pero para quien deba hacerlo, estos se incluyen por medio instancias de la clase System.Net.Attachment, que permite especificar un path o un stream como origen, el tipo de contenido (su mimetype) y algunas propiedades más. Estos objetos se adjuntan mediante el método Add de la colección Attachments del objeto MailMessage. Cuando pueda probar la clase con archivos adjuntos en un ambiente real, actualizo el código comentando si hubo algún issue al respecto.

Respecto al body en formato html, se puede comentar, aunque hablamos de SMTP y no de .Net, que si incluimos imágenes, nuestro html puede hacer referencia a imágenes hosteadas en algún server o a imágenes adjuntas, en cuyo caso se incluye solo el nombre de la imágen en el tag img.

Autenticación en un host con ssl

Donde hay muchas veces un gran problema, es en el uso de SSL para conectarse al host SMTP. Ocurre que para establecer el canal SSL el host utiliza un certificado, nuestra máquina cliente intentará validar ese certificado contra las autoridades certificantes registradas un su propio repositorio, y si no lo consigue arrojará una excepción con el fatídico texto “The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel” y dentro de ella otra con el más aclaratorio: “The remote certificate is invalid according to the validation procedure“. Esto generalmente ocurre porque el certificado no está emitido por una autoridad (CA) reconocida en nuestra máquina cliente, entonces podemos:

  • Obtener el certificado root de la CA e instalarlo la máquina donde correrá nuestro sistema para que esta reconozca el certificado del host SMTP (esto no pasa mucho más lejos de un click derecho en el archivo del certificado), normalmente le pediremos el mismo al administrador del host.
  • Solicitarle al responsable del host que pague un certificado de una CA reconocida, digamos VeriSign, etc. (esto siempre por teléfono o mail, nunca personalmente y si es el caso preparar antes un CV, por si nos quedamos sin cliente por insolentes)
  • Considerar que lo unico que nos interesa es que nuestro sistema envíe los condenados mails y forzar al framework a realizar una validación personalizada o a aceptar como válido cualquier certificado que el host posea (está última la opción del ejemplo)

Para ello (la tercera opción) el framework 2.0 nos expone una propiedad del objeto ServicePointManager, ServerCertificateValidationCallback, que nos permite especificar un delegado a una función que se encargue de validar el certificado a nuestro gusto, por ejemplo, retornando siempre true. Si quisieramos realizar una validación personalizada, como chequear el DN del certificado, deberíamos hacerlo aquí, ya que nuestro método de validación recibe el certificado en un parámetro, permitiendos acceder a todas sus propiedades.

Es importante destacar que esto último se aplica también a cualquier conexión http vía ssl realizada por el framework, llámese Web Service o HttpWebRequest.

Por último aquí un sitio dedicado al namespace System.Net.Mail y, si necesitamos tocar los headers del mensaje, este artículo de Scott Mitchell (cuando no) desde donde podemos acceder también a otros artículos de su autoría sobre envío de mails con .Net en sus diferente versiones.

Public Class Correo

    Public Shared Sub Enviar(ByVal MailDestinatarios As System.Net.Mail.MailAddressCollection, _
    ByVal MailDireccionOrigen As String, _
    ByVal MailNombreOrigen As String, _
    ByVal MailSubject As String, _
    ByVal MailBody As String, _
    ByVal MailHost As String, _
    Optional ByVal MailPort As Integer = 25, _
    Optional ByVal MailIsBodyHtml As Boolean = True, _
    Optional ByVal EnableSSL As Integer = 0, _
    Optional ByVal SSLuser As String = "", _
    Optional ByVal SSLpass As String = "")

        'Se crea el mensaje
        Dim oMensaje As New MailMessage

        'Se crea el cliente SMTP
        Dim oSMTP As New SmtpClient()

        'Se agregan las direcciones
        oMensaje.From = New MailAddress(MailDireccionOrigen, MailNombreOrigen)
        For Each addr As MailAddress In MailDestinatarios
            oMensaje.To.Add(addr)
        Next

        oMensaje.Subject = MailSubject
        oMensaje.Body = MailBody
        oMensaje.IsBodyHtml = MailIsBodyHtml
        'Seteo que el server notifique solamente en el error de entrega
        oMensaje.DeliveryNotificationOptions = DeliveryNotificationOptions.OnFailure

        'servidor smtp (dirección IP ó nombre de host)
        oSMTP.Host = MailHost
        'Puerto a utilizar (25 por defecto)
        oSMTP.Port = MailPort

        'Si se solicitó SSL, lo activo
        If EnableSSL = 1 Then
            oSMTP.EnableSsl = True
            'Bypass de validación de certificado (para problemas con servidores de SMTP con SSL con certificados que no validan en nuestra máquina)
            ServicePointManager.ServerCertificateValidationCallback = New RemoteCertificateValidationCallback(AddressOf ValidarCertificado)
        End If
        'Cargo las credenciales si hacen falta
        If Not String.IsNullOrEmpty(SSLuser) Then
            Dim credenciales As New System.Net.NetworkCredential(SSLuser, SSLpass)
            oSMTP.Credentials = credenciales
        End If

        Try
            oSMTP.Send(oMensaje)
        Catch ex As SmtpException
            Throw 'Aqui rutina de comprobación propia y logueo de excepción
        Catch ex As Exception
            Throw 'Aqui logueo de excepción
        Finally
            oMensaje = Nothing
            oSMTP = Nothing
        End Try
    End Sub

    Private Shared Function ValidarCertificado(ByVal sender As Object, ByVal certificate As X509Certificate, ByVal chain As X509Chain, ByVal sslPolicyErrors As System.Net.Security.SslPolicyErrors) As Boolean
        'bypass de la validación del certificado (aplicar aquí validación personalizada si fuera el caso)
        Return True
    End Function
End Class

Cuando actualice el código con utilización de archivos adjuntos y/o la versión en C# avisaré como comentario del artículo.

Saludos.