viernes, 20 de febrero de 2015

Un poco sobre HtmlAgilityPack

Tuve la necesidad de manipular un poco de HTML con C# y pues todas la búsquedas apuntaron a HtmlAgilityPack.

El problema

Por medio de un editor WYSIWYG, en modo HTML, permito la edición de contenido que se publica en un sitio web, no se trata de un CMS, pero los usuarios tienden a redactar el texto primero en Microsoft Word © y luego copiarlo al editor del sitio. El HTML que se forma incluía una serie de estilos que corrompía la maquetación del sitio.
El contenido se muestra de dos maneras, primero un breve resumen y luego el contenido completo. La dificultad se encuentra en el resumen, puesto que debe cortar el texto original podría provocar que una etiqueta quede mal construida o sin cerrar.

La solución

Lo primero que necesitaba era limpiar el HTML antes de almacenarlo en el base de datos. La limpieza consiste en eliminar todo los atributos de los elementos, el que más me molestaba era el atributo "style".

private string Clean(string html)
{
 HtmlDocument doc = new HtmlDocument();
 System.IO.StringWriter sw;
 string result;

 doc.LoadHtml(html);

 foreach(HtmlNode n in doc.DocumentNode.SelectNodes("*"))
 {
  if (n.HasAttributes)
  n.Attributes.RemoveAll();
 }
 
 sw = new System.IO.StringWriter();
 doc.Save(sw);
 doc = null;

 result = sw.ToString();

 sw.Dispose();
 sw = null;

 return result;
}

Ahora debía recortar el texto a los primeros 250 caracteres y garantizar tener un HTML bien formado. El código lo estoy utilizando desde una vista con Razor.

string tmp;
HtmlDocument doc;
System.IO.StringWriter sw;

doc = new HtmlDocument();

tmp = Model.info_a_publicar;

if (!string.IsNullOrWhiteSpace(tmp) && tmp.Length > 250)
{
 tmp = tmp.Substring(0, 250);
 tmp = tmp.Substring(0, tmp.LastIndexOf(" ")); //con esto garantizo de tener palabras completas

 sw = new StringWriter();

 doc.OptionFixNestedTags = true;
 doc.OptionAutoCloseOnEnd = true;
 doc.OptionCheckSyntax = true;
 doc.LoadHtml(tmp);
 doc.Save(sw);

 sw.Flush();
 tmp = sw.ToString();
 sw.Close();

 sw = null;
}

doc = null;

viernes, 15 de noviembre de 2013

DATEPART() + @@DATEFIRST = ?

En uno de esos días en los que busco como complicarme la vida me pregunté ¿Habrá una forma de saber el día de la semana de una fecha sin preocuparme por la configuración de SET DATEFIRST?

Imagen 1
Entonces decidí ver la diferentes valores que podían tomar los días de la semana para los valores posibles que puede tomar SET DATEFIRST. El resultado es la matriz que se muestra en la imagen 1.

Si configuramos SET DATEFIRST a 1 entonces el primer día de la semana será lunes, es decir que DATEPART(dw, ) nos devolverá 1 para lo lunes y  7 para los domingos.

Ahora si configuramos SET DATEFIRST a 2 entonces el primer día de la semana será martes, entonces DATEPART(dw, ) nos devolverá el 1 para los martes, 6 para los domingos y 7 para los lunes.



Si sumo los diferentes valores que puede tomar un lunes con los diferentes valores que toma la variable @@DATEFIRST tenemos lo siguiente.

@@DATEFIRST + DATEPART(dw, )
==> 1 + 1 = 2
==> 2 + 7 = 9
==> 3 + 6 = 9
==> 4 + 5 = 9
==> 5 + 4 = 9
==> 6 + 3 = 9
==> 7 + 2 = 9

Obtengo que para los lunes hay dos posibles valores 2 y 9.

Repito el mismo proceso para los miércoles:
@@DATEFIRST + DATEPART(dw, )
==> 1 + 3 = 4
==> 2 + 2 = 4
==> 3 + 1 = 4
==> 4 + 7 = 11
==> 5 + 6 = 11
==> 6 + 5 = 11
==> 7 + 4 = 11

Para los miércoles tengo dos posibles valores 4 y 11.

Ahora lo hago para los domingos :
@@DATEFIRST + DATEPART(dw, )
==> 1 + 7 = 8
==> 2 + 6 = 8
==> 3 + 5 = 8
==> 4 + 4 = 8
==> 5 + 3 = 8
==> 6 + 2 = 8
==> 7 + 7 = 8

Para los domingos solo hay un posible valor 8.

Concluyo que si el resultado de sumar @@DATEFIRST + DATEPART(dw, ) es:
  1. 2 ó 9 la fecha es un lunes
  2. 3 ó 10 la fecha es un martes
  3. 4 ó 11 la fecha es un miércoles
  4. 5 ó 12 la fecha es un jueves
  5. 6 ó 13 la fecha es un viernes
  6. 7 ó 14 la fecha es un sábado
  7. 8 la fecha es un domingo.
De esta forma no me preocupo de tener que usar SET DATEFIRST, sobre todo cuando toca modificar o agregar un subproceso a un proceso muy complejo. También me evito la necesidad que al final del subproceso de a restablecer SET DATEFIRST a su valor inicial.

martes, 12 de marzo de 2013

LINQ: IN (subquery)

Usar una subconsulta en un operdor IN es algo muy común para un programador, pero ¿Por qué es tan difícil encontrar un ejemplo claro de esto para LINQ to Entities?

Luego de darme a la tarea de buscar la solución para implentar el operador IN (subquery) con LINQ to Entities encontré este enlace http://stackoverflow.com/questions/3477918/linq-subquery-in. La solución es muy sencilla, pero me sorpendí porque en lugar de ver IN (subquery) ví un EXISTS(subquery) en la sentencia.

Eemplo:

LINQ
var subquery = from a in Bitacoras
 where a.env_numero == 5
 select a.asig_codigo;

var query = from b in Asignacions
 where subquery.Contains(b.asig_codigo)
 select b.asig_codigo;

T-SQL generado
SELECT 
[Extent1].[asig_codigo] AS [asig_codigo]
FROM [dbo].[Asignacion] AS [Extent1]
WHERE  EXISTS (SELECT 
 1 AS [C1]
 FROM [dbo].[Bitacora] AS [Extent2]
 WHERE (5 = [Extent2].[env_numero]) AND ([Extent2].[asig_codigo] = [Extent1].[asig_codigo])
)

No fue el EXISTS(subquery) lo que me sorprendió, es preferible usar el operador EXISTS en lugar de IN por que el primero tiene un mejor rendimiento. Lo que realmente me sorprendió es ver que LINQ to Entities generó una sentencia T-SQL optimizada para manejar grandes volúmenes de datos.

Sucede que el concepto que tengo es que para un proyecto sencillo, sin preocupción alguna, puedes hacer todas tus consultas con LINQ to Entities, pero es  muy probable que un futuro el volumen de datos crezca a niveles considerables y halla que trasladar ciertas consultas a procedimientos almacenados aplicando técnicas de optimización.

Ahora tendré toda confianza de usar consultas de LINQ parecidas a la anterior.

Para finalizar les dejo un ejemplo de LINQ to Entities usando el operador IN, pero con volores conocidos.

Ejemplo:

LINQ
var ids = new int[]{1,2,3,4,5};

var q = from b in Envios
  where ids.Contains(b.env_numero)
  select b;

T-SQL generado
SELECT 
[Extent1].[env_numero] AS [env_numero], 
[Extent1].[gtor_codigo] AS [gtor_codigo], 
[Extent1].[env_fecha] AS [env_fecha], 
[Extent1].[env_fecha_fin] AS [env_fecha_fin]
FROM [dbo].[Envio] AS [Extent1]
WHERE [Extent1].[env_numero] IN (1,2,3,4,5)

martes, 12 de febrero de 2013

Instalando desde la línea de comandos

Hace poco requería conseguir una manera de centralizar desde un mismo punto la instalación de los programas Gpg4win y MORFIS, este último esta desarrollo con .Net 1.1 por lo tanto es un requisito. Pero me urgía hacer esto, así que en ese momento creí que la forma más rápida para hacerlo era con un archivo .bat.

Para lanzar los instaladores solo necesitaba hacer uso del comando start.

start /wait gpg4win.exe
start /wait aspnet1_1.exe /Q
start /wait aspnet1_1sp1.exe /Q
start /wait morfis_setup.ex /qr

El modificador /wait obliga al command a esperar que termine de ejecutarse el exe, modificador /Q y /qr reduce la interacción del usuario con los instaladores. Para el caso del .Net 1.1 SP1 no se presenta ni pantalla.

Para evitar los ruidos de los exes y otros comandos se debe apagar los mensajes de salida con echo.

@echo off

Pero la instalación del .Net está condicionoda por su presencia, así que lo primero es saber como Windows me informa de la presencia de .Net 1.1, san Google me ayudo a encontrar ese dato en un artículo de MS. Era de esperarse que esa información estuviera en el registro de Windows, para poder consultar el regedit, Windows a puesto a mi disposición el comando reg query, pero necesitaba poder leer la salida del este comando así que hice uso de findstr.

reg query "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v1.1.4322" /v install | findstr 0x1

reg query "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v1.1.4322" /v SP | findstr 0x1

reg query "HKLM\SOFTWARE\Wow6432Node\Microsoft\NET Framework Setup\NDP\v1.1.4322" /v Install | findstr 0x1

reg query "HKLM\SOFTWARE\Wow6432Node\Microsoft\NET Framework Setup\NDP\v1.1.4322" /v SP | findstr 0x1

Tengo que hacer 4 consultas porque las llaves cambian según la arquitectura y se deben leer do valores de una llave, el primer valor es para saber si está instalado .Net 1.1 y el segundo valor para saber si ya tiene su SP1. El modificador /v del comando reg query le indica que busque un valor especifico en la llave especificada, el operador | (pipe) redirecciona la salida de reg query a la entrada de findstr al cual le indico que busque la cadena "0x1", si lo encuentra findstr retorna el código 0 de lo contrario 1.

Con un if puedo verificar el código de retorno de findstr y modificar la linea de ejecución con un goto.

reg query "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v1.1.4322" /v install | findstr 0x1
IF %errorlevel% == 0 SET netv1sp1=%netv1sp1%1

reg query "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v1.1.4322" /v SP | findstr 0x1
IF %errorlevel% == 0 SET netv1sp1=%netv1sp1%1

reg query "HKLM\SOFTWARE\Wow6432Node\Microsoft\NET Framework Setup\NDP\v1.1.4322" /v Install | findstr 0x1
IF %errorlevel% == 0 SET netv1sp1=%netv1sp1%1

reg query "HKLM\SOFTWARE\Wow6432Node\Microsoft\NET Framework Setup\NDP\v1.1.4322" /v SP | findstr 0x1
IF %errorlevel% == 0 SET netv1sp1=%netv1sp1%1

IF "%netv1sp1%" == "11" goto morfis
IF "%netv1sp1%" == "" goto ambos
IF "%netv1sp1%" == "1" goto servicepack

El codigo de retorno se almacena en la variable errorlevel si está es igual a cero uso el comando set para saber que tengo instalado en la PC, la variable netv1sp1 puede tener 3 valores. (Para obtener los valores de la variables estás se deben encerrar con  %):
  1. "11": Net 1.1 está instalado y tiene su SP1 así que paso directo a instalar MORFIS
  2. "1": Está instalado .Net 1.1 pero hace falta el SP1
  3. "": Hay que instalar .Net 1.1 y su SP1
Optimece el bat para que funcione en con Windows XP, no vaya hacer me encuentre con un cavernícola que aún lo use.

Para finalizar uso el comando pause.

El resultado final del bat es:

@echo off
echo ==============================================================
echo NO CIERRE ESTA VENTANA
echo ==============================================================
echo 1. Iniciando la Instalación
echo 1.2 Instalando GnuPG
start /wait GnuPG\gpg4win-light-2.1.0.exe

echo 1.3 Instalando Morfis

reg query "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v1.1.4322" /v install | findstr 0x1
IF %errorlevel% == 0 SET netv1sp1=%netv1sp1%1

reg query "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v1.1.4322" /v SP | findstr 0x1
IF %errorlevel% == 0 SET netv1sp1=%netv1sp1%1

reg query "HKLM\SOFTWARE\Wow6432Node\Microsoft\NET Framework Setup\NDP\v1.1.4322" /v Install | findstr 0x1
IF %errorlevel% == 0 SET netv1sp1=%netv1sp1%1

reg query "HKLM\SOFTWARE\Wow6432Node\Microsoft\NET Framework Setup\NDP\v1.1.4322" /v SP | findstr 0x1
IF %errorlevel% == 0 SET netv1sp1=%netv1sp1%1

IF "%netv1sp1%" == "11" goto morfis
IF "%netv1sp1%" == "" goto ambos
IF "%netv1sp1%" == "1" goto servicepack 

:ambos
echo 1.3.1 Instalando net v1.1
start /wait aspnet1_1.exe /Q

:servicepack
echo 1.3.1 Instalando net v1.1 sp 1
start /wait aspnet1_1sp1.exe /Q

:morfis
start /wait morfis_setup.exe /qr

echo INSTALACACIÓN COMPLETA
pause

miércoles, 30 de enero de 2013

BlackBerry10: Hub and Keyboard

Acabo de ver el vídeo del lanzamiento de BlackBerry 10 y las caratetísticas que más me gustaron fue BlackBerry Hub y BlackBerry Keyboard.

Tener centroalizado todas tus notificaciones de mensajaría, redes sociales, etc  y poder interactuar con ellas desde una misma aplicación está estupendo. El teclado se ve fuera de serie, estratosférico, realmente inteligente, sin duda será lo primero que pruebe cuando tengan el BlackBerry Z10 en mis manos.

Bueno solo resta esperar el comportamiento del mercado para saber si BlackBerry logrará reposicionarse en el mundo de los teléfonos inteligentes (smartphones).

viernes, 25 de enero de 2013

JavaScript: Mi mejor aliado



Es increible en todo lo que me ayuda JavaScript. Su último rescate fue ayudarme a obtener un sencillo reporte cuyos datos estaban en un grupo de  Google Apps. El panel de administración gratuito de Google Apps lista los miembros de los grupos de 30 en 30 y no te pemite navegar fácilmente entre las páginas. Lo datos que debía obtener eran el correo electrónico y la función del miembro dentro del grupo.



Listado de los miembro de un grupo en el panel de Google Apps
El problema es obtener los datos, con un simple copiar y pegar no me resultó; iba a tardar mucho acomando los datos de 8 páginas.  En la imagen de abajo veran el resultado en Notepad. El resultado en Excel no se lo muestro porque la situación se complica aun más.




Los datos depués de pegarlos en Notepad

Con la ayuda de un sencillo script que ejecuto desde la consola de las herramientas para desarrolladores de IE9 (F12) logré extraer lo datos, ubicarlos en una tabla al final de la página y luego a copiar y pegar en Excel para darle forma al reporte.

El resulado del script

Acá les dejo el script:

//Obtenemos todas las tablas que hay en la página
rTables = document.getElementsByTagName("TABLE");

//La tercera tabla es la que contiene los datos
rTable = rTables[3];

//Creamos una nueva tabla para acomodar los datos
rs = document.createElement("TABLE");

//Recorro los datos para acomodarlos
for(i = 2; i < rTable.rows.length-1; i++)
{
 r = rs.insertRow(-1);
 c = r.insertCell(-1);
 c.innerText = rTable.rows[i].cells[2].innerText
 c = r.insertCell(-1);
 c.innerText = rTable.rows[i].cells[3].innerText
}

//Inserto los datos reacomodados al final de la página
document.getElementsByTagName("BODY")[0].appendChild(rs);

miércoles, 5 de diciembre de 2012

SSIS y los MessageBox

Sigo en mi proceso de migración de los paquetes versión 2000 a 2008, en está ocasión me encontré con una características que me libera de una preocupación.

En muchas ocasiones cuando he estado desarrollando un paquete he utilizado un MessageBox desde una tarea de script, para depurar los valores de las variables del paquete. En una ocasión le sucedió a un colega que olvidó comentar la linea de código del MessageBox y actualizó el paquete en el ambiente de producción, esto provocó muchos problemas porque el paquete no terminaba de ejecutarse esperando su clic en el botón "Ok" del MessageBox y detenía todo la cola de procesos.

Pero con SSIS 2008 no se presenta ese problema, MS lo hizo más inteligente, ¿Sería  que con mucha frecuencía los desarrolladores dejaban MessageBox en sus paquetes? pues ahora se genera el siguiente error cuando se ejecuta un paquete con MessageBox

Description: System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. --->; System.InvalidOperationException: Showing a modal dialog box or form when the application is not running in UserInteractive mode is not a valid operation. Specify the ServiceNotification or DefaultDesktopOnly style to display a notification from a service application.
Cuidado, que si SSIS esta configurado para mostrar la notificaciones el MessageBox se invocará y se quedará esperando su clic para continuar.

El código para ejecutar el paquete:

DTExec 
  /sql Operaciones
  /set \Package.Variables[User::correo].Properties[Value];mi@correo.com
  /set \Package.Variables[User::file].Properties[Value];data.zip
  /set \Package.Variables[User::tipo].Properties[Value];mensual

viernes, 16 de noviembre de 2012

SSIS: NULL(DT_STR, «length», «code_page»)

Migrando un DTS de SQL Server 2000 a un paquete de SSIS 2008, me econtré dos situaciones muy interesante.

Escenario


El DTS se encarga de cargar un archivo de texto plano de una fuente externa a una tabla en SQL Server, en una columna tipo fecha se presenta una particularidad. El campo presenta tres valores diferentes:

  1. vacío, separador de columna continuo (,,) el cual se intepreta como valor nulo
  2. el texto NULL
  3. o una fecha con hora (01/01/2012 00:00:00)

DTS Preview
Imagen 1 - Columna con tres diferentes valores

Caso 1


Con el DTS de SQL Server 2000 los datos se cargan sin ninguna complicación, tanto vacío como NULL son intepretado como un valor nulo en la tabla. Esto no ocurre en el paquete SSIS 2008, sino que se genera un error "The value could not be converted because of a potential loss of data.", el motivo de este error es obvio, el texto NULL no se puede convertir a una fecha validad.

Datos en la Tabla
Imagen 2 - Datos importados a SQL Server


La solución a este caso fue utilizar una transformación Derived Column, esta me permite evaluar el valor de la columna problemática y generar uno nuevo a partir de este.


Caso 2

Como el problema era el valor NULL, por lo anto el objetivo consistió en usar la transformación Derived Column para remplazarlo por vacío, además se configuró la transformación para que sobreescriba el campo y no generar una columna nueva. El tipo de dato de la columna era string[DT_STR] esto es de esperarse porque la fuente de los datos es un archivo de texto plano.

En la primera expresión que escribí utilicé la función NULL(DT_STR, «length», «code_page»), esta función debería de devolver un valor vacío (nulo) válido para el tipo de dato de la columna origen.

[Column 53] == "NULL" ? NULL(DT_STR,50,1252) : [Column 53]


Pero la expresión generaba este error:

Error at Cargo datos [Derived Column [369]]: For operands of the conditional operator, the data type DT_STR is supported only for input columns and cast operations. The expression "[Column 53] == "NULL" ? NULL(DT_STR,50,1252) : [Column 53]" has a DT_STR operand that is not an input column or the result of a cast, and cannot be used with the conditional operation. To perform this operation, the operand needs to be explicitly cast with a cast operator.
¿Qué? El mensaje de error explica que el valor resultante debe provenir de una columna de entrada o o de una operación de conversión de tipos. Entonces, el tercer argumento del operador ternario ?: esta correcto porque el valor proviene de una columna de entrada de datos, pero el segúndo argumento es la función NULL() que no es un cast ni es un una columna de entrada, por lo tanto lo correcto es:

--ahora con cast
[Column 53]=="NULL"?(DT_STR,50,1252)NULL(DT_STR,50,1252):[Column 53]


Resumen

--incorrecto
[Column 53]=="NULL"?NULL(DT_STR,50,1252):[Column 53]
--cast incorreto
(DT_STR,50,1252)([Column 53]=="NULL"?NULL(DT_STR,50,1252):[Column 53])
--correcto
[Column 53]=="NULL"?(DT_STR,50,1252)NULL(DT_STR,50,1252):[Column 53]
´