[#Oferta Trabajo] Experience Design Manager

Os paso una oferta de trabajo de mi empresa, por si alguno pudiera estar interesado.

La oferta es para Avanade España: http://www.avanade.com

 

Si te preguntas qué tal es Avanade como destino, te comento que ha sido seleccionada por tercer año consecutivo entre las mejores empresas de Europa para trabajar.

http://www.topemployers.eu/TopEmployersEurope2012/C/tabid/5864/C/1394/Avanade.aspx

 

es-es

 

Van los datos de la oferta.

 

Buscamos una persona de más de 6 años de experiencia en la creación de interfaces, web sites y aplicaciones de gran escala

· Imprescindible, tener experiencia demostrada en las siguientes tecnologías: HTML5, CSS3, Javascript, jQuery, XAML Expresion Blend, Photoshop, Illustrator, Expression Design

· Así como una sólida trayectoria en actividades relativas a UX gestionando equipos de personas

· Habilidades en gestión de clientes

· Tener experiencia y conocimiento en la aplicación de las metodologías de diseño de interacción y de investigación para el desarrollo de software, web y móvil

· Facilidad de integración y participación con el equipo

 

Si estás interesado, envíame un correo a

Your gMail image

XSL for Dynamics CRM Plug-in Documentation

Apunto por aquí este XSL que me ha venido bien para generar la documentación del código de unos plug-in para Dynamics CRM.

Como ya sabréis, cuando Visual Studio compila un proyecto, además ofrece la opción de generar un fichero XML con todos los comentarios de las cabeceras de los métodos.

image

Imagen: Opción que hay que activar en VS2008 para que se genere el XML de documentación.

 

image

 

 

image

Imagen: XML de documentación generada por Visual Studio.

 

Este fichero puede ser tratado con herramientas como Sandcastle que nos permite generar a partir de este XML la documentación del proyecto usando el formato de la MSDN, u otro personalizado.

image

Imagen: Ejemplo de documentación generada con Sandcastle.

 

Estas herramientas están muy bien, pero… (siempre hay un pero) este formato puede no ser el que necesitemos. O las opciones que nos ofrezca la herramienta pueden no satisfacer nuestras necesidades.

Entonces lo que podemos hacer, en lugar de abrir el Word y empezar a escribir literatura, es crear un pequeño fichero XSL, que se encargue de filtrar los métodos que necesitamos y que genere la documentación en el formato preciso, a partir del XML que ya genera Visual Studio.

Además, de esta forma, podemos añadir algunos atributos adicionales, que en el caso de los plug-ins de CRM pueden venir muy bien, ahora veremos porqué.

image

Imagen: Elementos añadidos a la documentación que posteriormente reconocerá el XSL.

 

En este proyecto en concreto, necesitaba documentar sólo algunos métodos, además de indicar un identificador asociado a cada método, el requerimiento asociado, y la información de registro del plugin. Todo esto cumpliendo un formato específico, que no era el de la MSDN.

Toda esta información debía ir posteriormente en un Word de millones de páginas, que impreso quedaría fenomenal para coger polvo en la estantería.

image

Imagen: Fichero HTML generado por el XSL.

 

 

 

 

Una de las cosas que hemos ganado utilizando este enfoque ha sido dejar la documentación en el lugar en el que mejor está: junto al código. Hemos pasado de documentar en Word a documentar directamente en la cabecera de cada método. Si para ver la descripción de un parámetro debo abrir un Word tan grande que tarda 2 minutos en cargarse, lo más probable es que prefiera depurarlo para ver qué hace.

Para indicar qué clases se documentan, y qué clases no, se usa el elemento <pluginhandler>. Sólo aquellas clases que lo lleven aparecerán en la documentación.

Para los métodos, ocurre algo parecido. Sólo los métodos que lleven el elemento <plugin> serán documentados.

Pego finalmente el código XSL que genera el HTML de la imagen anterior.

 

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
    xmlns:mf="urn:my-scripts">
  <xsl:output method="html" indent="yes"/>
 
  <xsl:template match="/">
 
    <style>
      body{
      font-size:"9pt";
      font-family:"Arial";
      }
      .Table
      {
      border:solid black 1px;
      }
      .TableTR
      {
      border:none;
      }
      .TableTD
      {
      border-right:solid black 1px;
      border-bottom:solid black 1px;
      padding:5px;
      }
    </style>
 
    <xsl:for-each select="/doc/members/member/pluginhandler">
      <xsl:variable name="typeName" select="substring-after(../@name,'T:')" />
      <!-- parent node name attr-->
      <h4>
        <xsl:value-of select="@entity"/>
        <xsl:text> Plugins</xsl:text>
      </h4>
 
      <table border="0" cellspacing="0" cellpadding="0" width="700px" class="Table">
        <col width="100px" />
        <col width="100px" />
        <col width="200px" />
        <col width="300px" />
        <tr class="TableTR" style="background:#CCCCCC;">
          <td class="TableTD">
            <b>Requirements</b>
          </td>
          <td class="TableTD">
            <b>Plugin Id</b>
          </td>
          <td class="TableTD">
            <b>Registration Info</b>
          </td>
          <td class="TableTD">
            <b>Description</b>
          </td>
        </tr>
 
        <xsl:for-each select="/doc/members/member[starts-with(@name,concat('M:',$typeName))]">
          <xsl:if test="count(plugin)>0">
            <tr class="TableTR">
              <td class="TableTD">
                <xsl:call-template name="TextOrHyphen">
                  <xsl:with-param name="pText" select="requiremementid" />
                </xsl:call-template>
              </td>
              <td class="TableTD">
                <xsl:call-template name="TextOrHyphen">
                  <xsl:with-param name="pText" select="pluginid" />
                </xsl:call-template>
              </td>
              <td class="TableTD">
                <!--Registration Info -->
                <div style="font-size:x-small;">
                  <span style="text-decoration:underline">
                    <xsl:value-of select="plugin/@type"/>
                  </span>
                  <xsl:text> Plug-in</xsl:text>
                  <br />
                  <br />
                  <b>
                    <xsl:text>Events:</xsl:text>
                  </b>
                  <br />
                  <xsl:for-each select="event">
                    <xsl:text>- </xsl:text>
                    <xsl:value-of select="@name"/>
                    <xsl:text> (</xsl:text>
                    <xsl:value-of select="@type"/>
                    <xsl:text>)</xsl:text>
                    <br />
                  </xsl:for-each>
                  <br />
                  <b>
                    <xsl:text>Images:</xsl:text>
                  </b>
                  <br/>
                  <xsl:choose>
                    <xsl:when test="count(image)>0">
                      <xsl:for-each select="image">
                        <span style="text-decoration:underline">
                          <xsl:value-of select="@name"/>
                        </span>
                        <br />
                        <xsl:text>(</xsl:text>
                        <xsl:value-of select="@fields"/>
                        <xsl:text>)</xsl:text>
                        <br />
                      </xsl:for-each>
                    </xsl:when>
                    <xsl:otherwise>
                      <xsl:text>None required</xsl:text>
                    </xsl:otherwise>
                  </xsl:choose>
                </div>
                <!-- End Registration Info -->
              </td>
              <td class="TableTD">
                <xsl:copy-of select="summary" />
              </td>
            </tr>
 
            <!-- Parameters -->
            <xsl:if test="count(param)">
              <tr class="TableTD">
                <td colspan="4" style="background:#E0E0EB">
                  <b>
                    <xsl:text>Parameters</xsl:text>
                  </b>
                </td>
              </tr>
              <tr class="TableTD">
                <td colspan="4">
                  <!-- Parameters -->
                  <table border="0" cellspacing="0" cellpadding="0" width="700px" class="Table">
                    <col width="200px" />
                    <col width="150px" />
                    <col width="350px" />
 
                    <tr class="TableTR" style="background:#CCCCCC">
                      <td class="TableTD">
                        <b>Name</b>
                      </td>
                      <td  class="TableTD">
                        <b>Type</b>
                      </td>
                      <td  class="TableTD">
                        <b>Description</b>
                      </td>
                    </tr>
                    <xsl:choose>
                      <xsl:when test="count(param)=0">
                        <tr class="TableTR">
                          <td class="TableTD">-</td>
                          <td class="TableTD">-</td>
                          <td class="TableTD">-</td>
                        </tr>
                      </xsl:when>
                      <xsl:otherwise>
                        <xsl:for-each select="param" >
                          <tr class="TableTR">
                            <td class="TableTD">
                              <xsl:call-template name="TextOrHyphen">
                                <xsl:with-param name="pText" select="@name" />
                              </xsl:call-template>
                            </td>
                            <td class="TableTD">
                              <xsl:choose>
                                <xsl:when test="not(string(@type)='')">
                                  <xsl:value-of select="@type"/>
                                </xsl:when>
                                <xsl:otherwise>
                                  <xsl:value-of select="mf:GetParameterType(../@name, position())"/>
                                </xsl:otherwise>
                              </xsl:choose>
                            </td>
                            <td class="TableTD">
                              <xsl:copy-of select="." />
                            </td>
                          </tr>
                        </xsl:for-each>
                      </xsl:otherwise>
                    </xsl:choose>
                  </table>
                  <!-- End Parameters -->
                </td>
              </tr>
            </xsl:if>
            <!-- End Parameters -->
 
            <!-- Exceptions and Summary -->
            <xsl:if test="count(exception)>0 or count(remarks)>0">
              <tr class="TableTD">
                <td class="TableTD" colspan="4" style="background:#E0E0EB">
                  <b>
                    <xsl:text>Remarks</xsl:text>
                  </b>
                  <br />
                  <xsl:copy-of select="remarks"/>
                  <br />
                  <br />
                  <xsl:if test="count(exception)>0">
                    <b>
                      <xsl:text>Exceptions</xsl:text>
                    </b>
                    <br />
                    <xsl:for-each select="exception">
                      <u>
                        <xsl:value-of select="@cref"/>
                      </u>
                      <br />
                      <xsl:copy-of select="."/>
                    </xsl:for-each>
                  </xsl:if>
                </td>
              </tr>
            </xsl:if>
            <!-- End Exceptions and Summary -->
 
          </xsl:if>
        </xsl:for-each>
 
      </table>
 
    </xsl:for-each>
  </xsl:template>
 
  <xsl:template name="TextOrHyphen">
    <xsl:param name="pText" />
    <xsl:choose>
      <xsl:when test="string($pText)=''">
        <xsl:text>-</xsl:text>
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="$pText"/>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
 
  <msxsl:script language="C#" implements-prefix="mf">
    <![CDATA[ 
    
    public string SubstringAfterLast(string text, string delimiter)
    {
      string retVal = "";
      
      if(!string.IsNullOrEmpty(text)&&text.Contains(delimiter))
      {
          retVal = text.Substring(text.LastIndexOf(delimiter)+1);
      }
      return retVal;
    }
    
    // Splits a method signature and returns the value type of
    // the parameter in the specified position
    public string GetParameterType(string parametersString,int index)
    {
      string retVal = "";
      if(!string.IsNullOrEmpty(parametersString)&&parametersString.Contains("(")){
        parametersString=parametersString.Substring(parametersString.IndexOf('(')+1);
        parametersString=parametersString.Substring(0,parametersString.Length-1);
        string[] parameters = parametersString.Split(',');
        if(index<=parameters.Length)
        {
          retVal = parameters[index-1];
          if(!string.IsNullOrEmpty(retVal))
          {
            if(retVal.Contains("."))
            {
              retVal = retVal.Substring(retVal.LastIndexOf('.')+1);
            }
            retVal = retVal.Replace("}","?");
          }
        }
      }
      return retVal;
    }
    ]]>
  </msxsl:script>
 
</xsl:stylesheet>

 

Son 280 líneas, que aunque parezcan muchas, no se tarda demasiado en escribir. El XSL es un lenguaje muy… verboso (cuál es la traducción de verbose?)

Quizá lo más complicado ha sido obtener el tipo de datos de cada parámetro. Para esto está la función GetParameterType, que aunque está definida dentro del XSL es una función c#. Esta es una funcionalidad XSL no estándar. Sólo está disponible con el parser MSXML.

 

Para ejecutar el XSL se puede usar la opción de menú que aparece cuando abrimos el XSL con visual Studio.

image

La primera vez, Visual Studio nos preguntará qué XML de entrada queremos usar. Habrá que indicarle el XML de documentación que se encuentra en la carpeta bin del output de Visual Studio.

 

También puede hacerse desde la línea de comandos. Inicialmente, pensaba que con la utilidad msxsl.exe

http://www.microsoft.com/en-us/download/details.aspx?id=21714

image

 

 

 

 

Pero resulta que no, que esta utilidad no admite usar funciones en línea, como la que usamos para averiguar los tipos de los parámetros.

 

Por lo que necesitamos hacernos una aplicación de consola. Pero, es sencillito sencillito.

La clase que utilizamos para realizar la magia es XslCompiledTransform.

http://msdn.microsoft.com/en-us/library/42d26t30.aspx

 

Aplicación de consola para ejecutar la transformación:

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.XPath;
using System.Xml;
using System.Xml.Xsl;
 
namespace XslTransform
{
    class Program
    {
        static void Main(string[] args)
        {
            if (args.Length != 3)
            {
                Console.WriteLine("You have not entered the correct parameters");
                return;
            }
 
            string xmlfile = args[0];
            string xslfile = args[1];
            string outfile = args[2];
 
            try
            {
                using (XmlReader reader = XmlReader.Create(xmlfile))
                {
                    XmlWriterSettings wSettings = new XmlWriterSettings();
                    wSettings.ConformanceLevel = ConformanceLevel.Auto;
                    using (XmlWriter writer = XmlWriter.Create(outfile,wSettings))
                    {
                        // Create and load the transform with script execution enabled.
                        XslCompiledTransform transform = new XslCompiledTransform();
                        XsltSettings settings = new XsltSettings();
                        settings.EnableScript = true;
                        transform.Load(xslfile, settings, null);
 
                        // Execute the transformation.
                        transform.Transform(reader, writer);
                        writer.Close();
                    }
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }
    }
}

Primer parámetro: Path del fichero Xml con la documentación generado por Visual Studio

Segundo parámetro: Path del fichero Xsl que hemos construido.

Tercer parámetro: Path de salida. Sería el Xml con la documentación HTML Generada.

 

Y la llamada desde la línea de comandos sería así:

XslTransform.exe MyBusiness.Plugnis.xml PluginsDoc.xslt PluginsDoc.html

 

 

 

El código se puede descargar desde aquí

 

Referencias:

http://www.w3schools.com/xsl/

http://www.w3schools.com/xpath/

http://msdn.microsoft.com/en-us/library/ms256471.aspx

Dynamics CRM Plugin Deployment Batch

 

La tarea de depurar plugins en CRM es una de las tareas más engorrosas con las que me ha tocado bregar.

Cada vez que tocas el código de un plugin, hay que

recompilar,

parar el web de CRM,

parar el Asynchronous Processing Service,

copiar las dlls a la carpeta del servidor,

registrar en la GAC las dependencias,

iniciar el web y el servicio,

volver a probar.

 

Un engorro, vamos. Por eso, he creado este pequeño batch que hace todo eso en un click.

 

   1:  set PLUGINS_OUTPUT=C:ProjectsPluginsbinDebug
   2:  set CRM_SERVER_NAME=CRMSERVERNAME
   3:  set GACUTIL_PATH=C:Program FilesMicrosoft SDKsWindowsv6.0Abingacutil.exe
   4:  set SERVER_ASSEMBLY_LOCAL_PATH=C:Program FilesMicrosoft Dynamics CRMServerbinassembly
   5:  set SERVER_ASSEMBLY_NETWORK_PATH=\CRMSERVERNAMEC$Program FilesMicrosoft Dynamics CRMServerbinassembly
   6:   
   7:  REM STOP CRM WEB And Service
   8:  sc \%CRM_SERVER_NAME% stop MSCRMAsyncService
   9:  sc \%CRM_SERVER_NAME% stop w3svc
  10:   
  11:  REM Copy Files
  12:  copy "%PLUGINS_OUTPUT%Plugins.dll" "%SERVER_ASSEMBLY_NETWORK_PATH%" /y
  13:  copy "%PLUGINS_OUTPUT%Plugins.pdb" "%SERVER_ASSEMBLY_NETWORK_PATH%" /y
  14:  copy "%PLUGINS_OUTPUT%PluginsDependencies1.dll" "%SERVER_ASSEMBLY_NETWORK_PATH%" /y
  15:  copy "%PLUGINS_OUTPUT%PluginsDependencies2.dll" "%SERVER_ASSEMBLY_NETWORK_PATH%" /y
  16:   
  17:  REM Register files in GAC
  18:  psexec.exe \%CRM_SERVER_NAME% -c "%GACUTIL_PATH%" /if "%SERVER_ASSEMBLY_LOCAL_PATH%PluginsDependencies1.dll"
  19:  psexec.exe \%CRM_SERVER_NAME% -c "%GACUTIL_PATH%" /if "%SERVER_ASSEMBLY_LOCAL_PATH%PluginsDependencies2.dll"
  20:   
  21:  REM Delete GAC registered files
  22:  del "%SERVER_ASSEMBLY_NETWORK_PATH%PluginsDependencies1.dll"
  23:  del "%SERVER_ASSEMBLY_NETWORK_PATH%PluginsDependencies2.dll"
  24:   
  25:  REM START CRM WEB and Service
  26:  sc \%CRM_SERVER_NAME% start MSCRMAsyncService
  27:  sc \%CRM_SERVER_NAME% start w3svc

 

Variables de la cabecera:

PLUGINS_OUTPUT: Output de Visual Studio con las dll del plugin y sus dependencias.

CRM_SERVER_NAME: El nombre de red del servidor de CRM

GACUTIL_PATH: Ruta local de la utilidad GACUTIL. Sólo existe en el servidor si está instalado el SDK de .net, por tanto lo cogemos del equipo local.

SERVER_ASSEMBLY_LOCAL_PATH: Ruta Local del directorio bin de CRM del servidor.

SERVER_ASSEMBLY_NETWORK_PATH: La misma ruta, pero accesible desde la red. Al ser administradores deberíamos poder acceder al raíz usando C$.

 

La utilidad GACUTIL.exe permite registrar ensamblados en la caché de .net.

Es necesario tener permisos de administrador en el servidor de CRM.

También es necesario tener la utilidad psexec.exe de Windows Sys Internals. Esta utilidad permite ejecutar comandos en un servidor remoto. Como si fuera un telnet, pero más potente.

El comando SC.exe permite controlar servicios del servidor de forma remota.

Lo he probado contra un servidor de CRM 4. No sé qué tal irá contra un CRM 2011.

Referencias:

http://technet.microsoft.com/en-us/sysinternals/bb897553.aspx
http://blogs.iis.net/davcox/archive/2009/07/14/where-is-gacutil-exe.aspx
http://msdn.microsoft.com/en-us/library/ex0ss12c%28v=vs.80%29.aspx
http://webhub.com/dynhelp:viewTn::catalog=TnNote,id=recycledefaultapppool
http://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/sc.mspx?mfr=true

Javascript Private Shared Variables

 

 

Apunto este trozo de código pues contiene un concepto interesante que puede ayudar cuando trabajas con Javascript.

Esta técnica permite definir una variable, cuyo ámbito se limita a una función, pero cuyo valor se comparte entre distintas llamadas.

Se parece a las variables privadas estáticas que existían en Visual Basic 6. La idea es la misma.

   1:  function MyFunction() {
   2:      if (!MyFunction.Value1) { // Set the default Value
   3:          MyFunction.Value1 = "Default Value";
   4:      }
   5:      else {
   6:          // The value has been already set   
   7:          MyFunction.Value1 = "Modified Value";
   8:      }
   9:   
  10:      return MyFunction.Value1;
  11:  }
  12:   
  13:  alert("1:" + MyFunction());
  14:  alert("2:" + MyFunction());
  15:   
  16:  // The value is not accesible from outside
  17:  MyFunction.Value1 = "Outside modification";
  18:   
  19:  alert("3:" + MyFunction());

Pruébalo en JS Fiddle: http://jsfiddle.net/69bdw/

 

Básicamente, lo que hacemos es tratar a MyFunction como si fuera un objeto, y asignarle una propiedad.

El valor Value1 existe dentro del contexto de MyFunction, y sólo es modificable desde dentro de él. Además su valor persiste entre llamadas.

Una aplicación interesante de este concepto podría ser una función, que atendiese un evento que se dispara repetidamente, pero para el que sólo realizaremos una acción cada cierto tiempo. Por ejemplo, el resize de la ventana.

 

HTML:

   1:  <div style="height:100px;border:solid 2px;">
   2:      <div id='div1' style="position:relative;background-color:red;width:50px;height:50px;top:25px;" />
   3:  </div>

JS:

   1:  function SetupPosition(){
   2:      var div1 = document.getElementById('div1');
   3:      var parentContainer = div1.parentElement;
   4:      var leftValue = (parentContainer.offsetWidth / 2) -
   5:                      (div1.offsetWidth / 2);
   6:      div1.style.left = leftValue+"px";
   7:  }
   8:   
   9:  function OnWindowResize() {
  10:      /// <summary>Handler for the window resize event</summary>
  11:      
  12:      if (OnWindowResize.TimerId) { // If the timer has already been set
  13:          // Clear it in order to avoid multiple executions
  14:          window.clearTimeout(OnWindowResize.TimerId);
  15:      }
  16:      // Set a timeout to calculate the element position
  17:      OnWindowResize.TimerId = window.setTimeout(SetupPosition, 500);
  18:  }
  19:   
  20:  // Handle the window resize Event
  21:  window.onresize = OnWindowResize;

Pruébalo en JS Fiddle: http://jsfiddle.net/3DNBk/

 

La función OnWindowResize comprueba que exista una ejecución previa del window.setTimeout “mirando” en su propiedad TimerId.

Si la hay, cancela la ejecución, y vuelve a planificar otra para dentro de medio segundo.

HTML Base Tag. Postback en ventanas modales

image

Me he encontrado más de una vez con el siguiente problema: Si se produce un postback en una ventana modal, el resultado de ese postback se carga en una nueva ventana, en lugar de hacerlo en la propia ventana modal.

Esto ocurre independientemente del evento que cause el postback: El click de un botón, el cambio de valor de un desplegable, etc.

Este comportamiento sólo ocurre con Internet Explorer.

Solución? este maravilloso Tag HTML:

base target=”_self”

image

Con ponerlo, se corrige el problema.

 

Y ya que estamos, qué hace ese tag?

Indica qué frame, o ventana se usará por defecto para cargar los enlaces. Esto, está pensado para aquellas ventanas que tienen varios elementos <iframe>.

Los iframes, tienen un atributo name, que sirve para identificarlos. Además, los iframes se pueden anidar.

Este tag tiene la misma función que el atributo target de la etiqueta <a>, la que sirve para especificar los hyperlinks.

Debe ser que en las ventanas modales, internet explorer asigna por defecto el valor _blank a este atributo.

Posibles valores del atributo target:

Valor Descripción
_blank Abre el enlace en una nueva ventana o pestaña
_self Abre el enlace en el mismo frame en que se encuentra el enlace
_parent Abre el enlace en el parent frame, o contenedor del frame en el que se encuentra el enlace
_top Si la ventana actual tiene varios iframes, los ignora todos y carga el enlace en la ventana actual.
framename Abre el enlace en el frame con el nombre especificado.

 

Adjunto código que muestra el funcionamiento y la corrección

 image

image

Enlaces:
http://www.w3schools.com/tags/att_base_target.asp
http://www.w3schools.com/tags/tag_a.asp
http://www.w3schools.com/tags/tag_iframe.asp

JSTreeGraph. HTML4+Javascript Tree Graph

image 
Imagen: Árbol en modo Horizontal.

 

image 
Imagen: Árbol en modo Vertical.

 

Llevo tiempo buscando algún componente que me permita representar en un entorno web, información jerárquica en cajitas. Hasta hoy no he dado con ninguna opción que cubra mis requerimientos; esto es, que no necesite plug-ins adicionales (flash, silverlight, etc.), que no haya que pagar licencias, y que permita mostrar la jerarquía tanto en vertical como en horizontal.

Estudiando detenidamente esta solución existente, me he de dado cuenta de que la lógica que hay detrás de estos árboles no parece muy complicada. Básicamente se trata de pintar una cajita por cada nodo, y unas líneas que las unan.

image 
Imagen: Componente Silverlight de YWorks

 

Así que, ya se sabe, a reinventar la rueda; me puse a programarlo. Y he llegado a la conclusión de que es más difícil de lo que parece. Aunque tampoco es para tanto 🙂 Eso sí, se aprende un montón.

Las dificultades con las que me he encontrado son:

Dibujar líneas sobre una capa en HTML: Con HTML5 y los lienzos, es fácil. Pero con HTML4, hay que aplicar algún truquillo.

El Layout, o dónde va cada cajita: Dar con un algoritmo, medianamente eficiente que permita conocer la posición de cada cajita, me ha traído de cabeza, pero ha salido… y creo que no ha quedado mal.

 

Dibujar líneas sobre una Capa HTML.

Investigando un poco por ahí, he dado con un par de artículos que comentaban el problema. Básicamente hay tres posibilidades para resolverlo:

image

Dibujar una capa de un pixel por un pixel en cada punto de la línea. El inconveniente evidente de esta técnica es que es muy ineficiente.

Usar una imagen con una línea en diagonal, hacia arriba o hacia abajo, y redimensionarla hasta ajustarla a los puntos necesarios. El inconveniente de esta técnica es que las líneas quedan muy pixeladas. La ventaja es que es muy eficiente.

Usar un div que tenga la longitud total de la línea. Esto sólo funciona para líneas rectas. La ventaja es que no se requieren imágenes adicionales, y es muy óptimo. Es la técnica que he utilizado, ya que, al menos en esta primera versión, sólo permitiré unir las cajitas con líneas rectas.

 

image

Lo que hacemos en esta función es crear una capa de lineWidth píxeles de grosor, con una longitud o determinada por la diferencia de posición de los puntos X e Y de los parámetros.

Como podemos ver, es bastante sencillo. Quizá lo más destacable es la manera en que se conoce si la línea es vertical u horizontal: si la coordenada X del punto de inicio de la línea es la misma que la coordenada X del punto final, entonces se trata de una línea vertical.

 

image

 

El Layout, o la disposición de las cajitas.

Para este tema, que ha costado bastante, he usado varias funciones recursivas, que establecen la posición de cada nodo.

El posicionamiento es distinto si pintamos el árbol en modo horizontal o vertical. Explicaré el posicionamiento Horizontal, pues parece más fácil de ver. Comprendido éste, el posicionamiento Vertical es exactamente igual, pero jugando con la posición superior de las cajitas, en lugar de la posición izquierda.

Como es necesario pasearse por la estructura del árbol, y calcular constantemente el nivel, la referencia al nodo padre, y al nodo que está a la izquierda en el mismo nivel (LeftNode), he creado una función que realiza todo este cálculo una única vez, al principio. Esta función es la función PrepareNode.

image

El LeftNode de un nodo es el que está inmediatamente a la izquierda en el mismo nivel. De este modo puedo navegar por el árbol de arriba a abajo, entre niveles, mediante las propiedades ParentNode y Nodes, y horizontalmente dentro de un mismo nivel mediante la propiedad LeftNode.

El parámetro rightLimits, es un array que contiene para cada nivel, el último nodo hijo analizado. Es un atajo para conocer cuál es LeftNode que está en una rama distinta de la que estoy analizando.

 

image

Habiendo pues, analizado cada nodo, y realizado este cálculo previo, pasmos a calcular las posiciones de cada cajita.

La función PerformLayout(node)

Esta es una función también recursiva. Su función es establecer las coordenadas de posición y el tamaño de cada una de las cajitas.

El parámetro de entrada de esta función es un nodo.

(1) Para conocer la posición de un nodo, lo primero que hemos de saber es si tiene hijos; si no los tiene (6) la posición izquierda será la del nodo inmediatamente a la izquierda, más un margen predefinido.

(2) Si el nodo tiene hijos, entra en juego la recursividad, pues volvemos a llamar a la función para cada uno de los hijos. Esta característica hace que se analicen en primer lugar los nodos finales de cada rama. Y esto está bien, pues normalmente la posición de un nodo irá condicionada por la posición de los elementos que tiene por debajo. Si el nodo no tiene hijos en cambio, el se pinta al lado del que tenga a la izquierda.

 

image

 

Una vez analizados los hijos, (3) utilizamos los resultados del paso anterior, localizamos el centro, y posicionamos el nodo en ese punto.

image

Puede ocurrir que ese centro se superponga con el nodo de la izquierda(4), con lo que hay que mover a la derecha(5) el nodo en cuestión, y todos sus descendientes.

image 
Imagen: El centro de los nodos hijos se superpone con el de la izquierda.

A continuación, (7) queda calcular la posición superior, el ancho y alto de cada cajita. La posición superior va en función del nivel, y el ancho y alto son valores constantes.

Por último, debemos indicar las coordenadas de los puntos de anclaje de los conectores(8). ¿Y qué es esto? Son los puntos donde está el inicio de la línea en caso de un nodo padre, y el fin de la línea en caso de un nodo hijo. Conociendo la izquierda, derecha, ancho y alto de cada nodo, este valor es muy fácil de calcular.

image
Imagen: Puntos de Anclaje.

Una vez calculadas las posiciones, ya sólo es cuestión de recorrerlos y pintar en cada posición una cajita. Y por último las líneas que conectan padres e hijos.

image 
Imagen: Flujo Completo.

Este mismo algoritmo es el que se utiliza para calcular las posiciones de los nodos en el modo Vertical.

 

El código fuente y los ejemplos están publicados en Codeplex: http://jstreegraph.codeplex.com

 

Referencias:

http://www.p01.org/releases/Drawing_lines_in_JavaScript/

http://www.yworks.com/products/yfilessilverlight/Demo.yFiles.Graph.Collapse.html

http://jsdraw2d.jsfiction.com/

http://en.wikipedia.org/wiki/Graph_drawing

Arrays vs Dictionaries

Tengo la necesidad de cargar en memoria una tabla con un gran volumen de datos. Hablamos de una tabla de millones de registros. Además la estructura de los datos no es conocida a priori. Los tipos de cada columna y su número se conocen después de ejecutar una consulta a base de datos.

Entonces me surge la duda: Qué sería más óptimo en cuanto al consumo de memoria? ¿Usar Arrays o Diccionarios?

La ventaja principal de los diccionarios es que me permite añadir filas a la tabla sin tener que preocuparme por redimensionar cada vez el array. Además, puedo acceder a los valores de la tabla usando un nombre, en lugar de usar un índice.

image

Para salir de dudas, he hecho una prueba, un poco simplona, pero que me ha sacado de dudas:

Primero, he hecho una carga en memoria de una tabla de 100 columnas y un millón de registros, usando un diccionario y una lista genérica.

image

Al ejecutar esto…

image

Vaya, nisiquiera llega a cargar el millón de registros. Se ha quedado  en 185.000 filas. 

image

A ver qué pasa con los arrays…

Lo mismo, pero con menos registros

image

image

Vuelvo a ejecutar la prueba, para cada una de las opciones, pero esta vez reduciendo el número de columnas a 10.

Esta vez, ambos procesos acaban sin el error de memoria.

El consumo de memoria es en el caso de los arrays es de 916MB

image

En el caso de las listas y diccionarios es de 929MB

image

Lo que sí he notado, pero no he querido entretenerme en comprobarlo, es que en el caso de los arrays el tiempo de ejecución era menor, lo que indica un acceso a memoria más óptimo en el caso de los arrays.

De todos modos me he decantado por usar las listas, pues la prioridad era el consumo de memoria frente al tiempo de proceso.

Hemos visto que el consumo de memoria es el prácticamente el mismo en ambos casos, pero usando los diccionarios, la programación es más cómoda, pudiéndose referenciar columnas por su nombre. Es posible que esto último ayude incluso a reducir el número de errores.

Convert ListaGenerica a DataTable

image

 

Me apunto por aquí un trozo de código que permite convertir una Lista Genérica (por ejemplo de entidades) a un DataTable. Es totalmente compatible con el Framework 2.

Para convertir una Lista Genérica en un DataTable, debemos primeramente obtener por reflexión las propiedades públicas de los elementos de la lista genérica. Cada una de estas propiedades corresponderá a una columna de la Tabla.

A continuación, deberemos recorrer los elementos de la lista genérica, y añadir una fila a la tabla por cada item de la lista.

Hay que tener cuidado con los tipos nullables. No están soportados en los DataTables.

 

/// <summary>
/// Convert data from a generyc list to a DataTable
/// </summary>
public static DataTable Convert<TItemType>(List<TItemType> list)
{
    DataTable convertedData = new DataTable();

    // Get List Item Properties info
    Type itemType = typeof(TItemType);
    PropertyInfo[] publicProperties =
        // Only public non inherited properties
        itemType.GetProperties(BindingFlags.Instance | BindingFlags.Public);

    // Create Table Columns
    foreach (PropertyInfo property in publicProperties)
    {
        // DataSet does not support System.Nullable<>
        if (property.PropertyType.IsGenericType &&
            property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
        {
            // Set the column datatype as the nullable value type
            convertedData.Columns.Add(property.Name, property.PropertyType.GetGenericArguments()[0]);
        }
        else
        {
            convertedData.Columns.Add(property.Name, property.PropertyType);
        }
    }

    // Convert the Data
    foreach (TItemType item in list)
    {
        object[] rowData = new object[convertedData.Columns.Count];
        int rowDataIndex = 0;
        // Iterate through Item Properties
        foreach (PropertyInfo property in publicProperties)
        {
            // Add a single cell data
            rowData[rowDataIndex] = property.GetValue(item, null);
            rowDataIndex++;
        }
        convertedData.Rows.Add(rowData);
    }

    return convertedData;
}

 

Y la operación inversa; Convertir una DataTable en una lista Genérica.

En este caso, lo primero será ver qué columnas de la tabla son propiedades públicas en los elementos de la lista. Las demás, no podré convertirlas, o al menos no directamente.

A continuación, recorremos las filas de la tabla, e instanciamos un elemento de la lista por cada fila. Asignamos por reflexión los valores a las propiedades, y añadimos la nueva instancia a la lista.

Este código también tiene en cuenta las conversiones entre Null y System.DBNull. Recordemos que en los DataTable se utiliza este valor para indicar un valor nulo. Esto seguramente es así, porque los DataTable son anteriores a la aparición de los tipos Nullables.

 

/// <summary>
/// Fills a generyc list with the data cointained in a DataTable
/// </summary>
public static List<TItemType> Convert<TItemType>(DataTable dt) where TItemType:new()
{
    List<TItemType> convertedData = new List<TItemType>();

    // Get List Item Properties info
    Type itemType = typeof(TItemType);
    
    // Only public non inherited properties
    BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public;

    // wich columns of the datatable are properties of TItemType?
    Dictionary<string,PropertyInfo> availableProperties = new Dictionary<string, PropertyInfo>();
    foreach (DataColumn column in dt.Columns)
    {
        PropertyInfo prop = itemType.GetProperty(column.ColumnName, bindingFlags);
        if(prop!=null) availableProperties.Add(column.ColumnName,prop);
    }

    // Fill the generyc list with data
    foreach (DataRow row in dt.Rows)
    {
        TItemType item = new TItemType();
        foreach (KeyValuePair<string, PropertyInfo> availableProperty in availableProperties)
        {
            object propValue = row[availableProperty.Key];
            if (propValue!=null&&propValue!=System.DBNull.Value)
                availableProperty.Value.SetValue(item, propValue, null);                

        }
        convertedData.Add(item);
    }

    return convertedData;
}

 

Por último, cómo probaríamos este código? Unit Testing a Tope!

Por ejemplo, puedo declarar una entidad de prueba, cargar una lista con datos aleatorios, y ver que tras la conversión a DataTable, la Tabla tiene tantas columnas como propiedades públicas tiene la entidad, y que el número de elementos coincide.

Y al hacer la operación inversa, ver que el número de elementos es el mismo.

 

List<TestEntity> myItems = new List<TestEntity>();

// Generate Random Data
Random r = new Random(System.DateTime.Now.Millisecond);
for (int i = 0; i < 10; i++)
{
    TestEntity newItem = new TestEntity();
    newItem.Name = Guid.NewGuid().ToString();
    newItem.Birthday = DateTime.Now.AddHours(r.Next(1000));
    newItem.LastBillDate = r.Next()%2 == 0 ? null : new DateTime?(DateTime.Now.AddHours(r.Next(1000)));
    newItem.LastBillAmount = r.Next() % 2 == 0 ? null : new double?(r.NextDouble());
    myItems.Add(newItem);
}

DataTable dt1 = Converter.Convert<TestEntity>(myItems);
// The date table columns count and the public properties must be the same
Assert.AreEqual(dt1.Columns.Count,typeof(TestEntity).GetProperties(BindingFlags.Instance | BindingFlags.Public).Length);
Assert.AreEqual(dt1.Rows.Count,myItems.Count);

// Re convert to an Entity List
List<TestEntity> myConvertedItems = Converter.Convert<TestEntity>(dt1);
Assert.AreEqual(myItems.Count, myConvertedItems.Count);

Deshabilitar Boton Guardar en PDF

image

 

Qué duda cabe que el formato PDF se ha popularizado tanto, que ha llegado a convertirse en un estándar en el intercambio de documentos. Hoy en día, cualquiera de las aplicaciones que manejamos permite exportar información usando este formato.

Muchas veces, nos encontramos con usuarios que manejan información confidencial, y desean que sus documentos estén de alguna manera controlados. Que a ser posible, no salgan de sus instalaciones. Que sus empleados, o las personas que tengan acceso a dicha información, sólo puedan visualizarla, pero que no tengan la posibilidad de copiarla y distribuirla.

A día de hoy, esta es una funcionalidad que es bastante complicada conseguir. Y lo es, porque en teoría, cualquier contenido al que tengamos acceso, es susceptible de ser copiado y posteriormente distribuido.

El formato PDF no es ajeno a esta característica. Una vez puestos los medios para impedir que personas no adecuadas tengan acceso al documento, (porque en impedir el acceso sí se pueden poner muchas medidas de seguridad) y el  usuario ha conseguido visualizarlo en pantalla, hay poco que podamos hacer; Si están los bits, estos se pueden copiar. Y si se pueden copiar, se pueden distribuir.

 

Se puede usar el viejo truco de imprimir la pantalla, imprimir el documento y volver a escanearlo, copiarlo con lápiz y papel, memorizarlo, hacer una foto con el móvil, y un montón de cosas que están ahí, y no son complicadas de abordar.

image

Como contramedida, lo que podemos hacer, es al menos, eliminar los medios de distribución más evidentes, como el botón guardar del Reader.

Existen parámetros que pueden pasarse al Reader para deshabilitar las barras de herramientas, el menú, la barra de estado, etc..

Se puede además indicar qué página del documento se mostrará, el nivel de zoom, incluso si queremos realizar automáticamente una búsqueda dentro del documento.

Referencia a la lista de parámetros disponibles:

http://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/pdfs/pdf_open_parameters.pdf

 

Los parámetros pueden pasarse por API, por línea de comandos, por URL, o dentro del tag object de un documento html.

Por ejemplo:

Acrobat.exe /A «zoom=1000» «C:example.pdf»

http://example.org/doc.pdf#page=3&pagemode=thumbs

 

image

Con Toolbar.

 

image

Sin Toolbar.

Existen también otras alternativas. Como usar viewers en los que podemos controlar ciertos parámetros, como el de las toolbars, el menú contextual, etc.

Van dos ejemplos de viewers en Silverlight.

http://www.pdftron.com/silverdox/

image

PDF Tron. Silverlight. Sin Toolbar. El menú contextual no permite copiar.

 

http://firstfloorsoftware.com/documenttoolkit

image

First Floor Document Toolkit for Silverlight. Impide guardar el documento, copiarlo, pegarlo, etc.

 

http://es.wikipedia.org/wiki/PDF

 

 

En cuanto a impedir que la información llegue a manos equivocadas, tenemos a nuestra disposición, el servidor de Rights Management.

http://www.microsoft.com/windowsserver2003/technologies/rightsmgmt/default.mspx

Windows Rights Management impide que los documentos protegidos puedan ser abiertos, impresos, reenviados, etc, a personas no autorizadas.

image

image

Como conclusión, yo diría que cuando nos encontremos con un requerimiento de este tipo, lo mejor es informar al usuario de los riesgos existentes y de las alternativas viables. El riesgo de fuga de información siempre estará ahí, y en última instancia, no existe más remedio que intentar restringir al máximo la información confidencial y concienciar al usuario de los riesgos que supone no tenerla controlada. Normalmente, antes de enseñar información confidencial, se suele pedir al usuario que acepte unos términos de NDA (Non Disclosure Agreement)