• Shortcuts : 'n' next unread feed - 'p' previous unread feed • Styles : 1 2

» Publishers, Monetize your RSS feeds with FeedShow:  More infos  (Show/Hide Ads)


Date: Tuesday, 29 Jul 2014 12:19

Esta vez el retraso viene de que la semana la terminé en Noja, quería aprovechar el trayecto en autobús para escribir la retro, pero casualmente coincidí con Fernando y entre unas cosas y otras al final pasé de hacerlo.

Cosas del verano, supongo, esta semana no encuentro nada en especial que destacar más allá del trabajo hecho:

  • En Nort terminé el trabajo de integración con facebook en el backend y empecé a trabajar en la versión móvil (una aplicación híbrida), estudiando el framework javascript que usamos y empezando la integración con la autentificación con facebook en este escenario
  • Tras la primera propuesta del rediseño de la web de minchador (que me moló bastante), tocó tratar algunos cambios y añadidos para poder seguir avanzando.
  • Estuve dedicando algún rato a mhop, cuestiones de organización más que otra cosa, además de preparar el viaje.

Buena semana.

Author: "--"
Send by mail Print  Save  Delicious 
Date: Tuesday, 29 Jul 2014 04:57
Expresso Logic, una interesante propuesta para realizar aplicaciones de manera consistente y rápidas

Modern Analyst, Expresso Logic
Author: "noreply@blogger.com (JorgeBec)"
Send by mail Print  Save  Delicious 
Date: Monday, 28 Jul 2014 11:50
Pues llegó la mejor época del año, prácticamente la única en la que algunos afortunados podemos alejarnos un poco de los mundos binarios y dedicarnos a otros menesteres seguro más importantes, como disfrutar de la familia y descansar, al menos unos días.

Variable not found quedará a la deriva hasta septiembre, cuando volveremos al ataque con las pilas cargadas y listos para afrontar la nueva temporada :)

¡Feliz verano, amigos!

Author: "noreply@blogger.com (José M. Aguilar)"
Send by mail Print  Save  Delicious 
Date: Saturday, 26 Jul 2014 19:00
Apache Tapestry

Un Content Delivery Network (CDN) no es más que un servidor, servidores o servicio dedicado a servir el contenido estático o actuar de cache para los clientes. Alguno de los motivos por los que podríamos querer usar un CDN en una aplicación son:

  • Algunos servicios CDN están repartidos geográficamente por el mundo de modo que el contenido sea servido de un lugar más cercano al usuario esto hace que el tiempo que tarda en cargar un página o servirse el contenido sea menor.
  • Descargar la tarea de servir al menos parte del contenido de la aplicación al CDN hará que no nos tengamos que preocupar de tener la capacidad para servirlo. Cuando se cargar una página se hacen varias peticiones al servidor para obtener el contenido como el html, imágenes, estilos, … haciendo que los contenidos estáticos sean servidos por el CDN hará que el servidor tenga menos carga, dependiendo del número de usuarios de la aplicación o los picos de tráfico notaremos una mejoría.
  • La alta fiabilidad de servicio que ofrecen.

Amazon ClodFront es una de las opciones que podemos usar como CDN. En este artículo voy a comentar como tener un CDN para servir el contenido estático en una aplicación que emplee el framework de desarrollo de aplicaciones web en la plataforma Java Apache Tapestry.

Para que el contenido estático se sirva del CDN debemos hacer que las URL de las imágenes y hojas de estilo se generen con la URL propia del CDN, al menos, deberemos cambiar el host de esas URL. No hay que hacer mucho más ya que CloudFront creo que se puede configurar para que cuando le lleguen las peticiones del contenido si no las tiene las delegue en la aplicación, una vez que las tiene cacheadas ya no necesita solicitarselas a la aplicación y las sirve él mismo.

Una de las cosas muy interesantes de Tapestry es que podemos modificar prácticamente cualquier comportamiento del mismo, esto es debido a que la mayor parte de sus funcionalidades son ofrecidas mediante servicios que podemos sobreescribir con los que nosotros proporcionemos, el contenedor de dependencias (IoC) de tapestry lo hace muy fácil. Para modificar las URL de los recursos estáticos que son generados en Tapestry deberemos implementar la clase AssetPathConverter. Una implementación podría ser la siguiente:

<noscript><pre><code>package es.com.blogspot.elblogdepicodev.plugintapestry.misc; import java.util.Map; import org.apache.tapestry5.ioc.annotations.Inject; import org.apache.tapestry5.ioc.annotations.Symbol; import org.apache.tapestry5.ioc.internal.util.CollectionFactory; import org.apache.tapestry5.services.AssetPathConverter; import es.com.blogspot.elblogdepicodev.plugintapestry.services.AppModule; public class CDNAssetPathConverterImpl implements AssetPathConverter { private String protocol; private String host; private String port; private String path; private Map&lt;String, String&gt; resources = CollectionFactory.newMap(); public CDNAssetPathConverterImpl(@Inject @Symbol(AppModule.CDN_DOMAIN_PROTOCOL) String protocol, @Inject @Symbol(AppModule.CDN_DOMAIN_HOST) String host, @Inject @Symbol(AppModule.CDN_DOMAIN_PORT) String port, @Inject @Symbol(AppModule.CDN_DOMAIN_PATH) String path) { this.protocol = protocol; this.host = host; this.port = (port == null || port.equals(&quot;&quot;)) ? &quot;&quot; : &quot;:&quot; + port; this.path = (path == null || path.equals(&quot;&quot;)) ? &quot;&quot; : &quot;/&quot; + path; } @Override public String convertAssetPath(String assetPath) { if (resources.containsKey(assetPath)) { return resources.get(assetPath); } String result = String.format(&quot;%s://%s%s%s%s&quot;, protocol, host, port, path, assetPath); resources.put(assetPath, result); return result; } @Override public boolean isInvariant() { return true; } }</code></pre></noscript>

También deberemos añadir un poco de configuración al módulo de la aplicación para que se use esta nueva implementación. Esto se hace en el método serviceOverride de la clase AppModule.java, donde también en el método contributeApplicationDefaults configuramos los símbolos que se usarán al generar las URLs entre ellos el dominio del CDN.

<noscript><pre><code>package es.com.blogspot.elblogdepicodev.plugintapestry.services; ... public class AppModule { private static final Logger logger = LoggerFactory.getLogger(AppModule.class); public static final String CDN_DOMAIN_PROTOCOL = &quot;cdn.protocol&quot;; public static final String CDN_DOMAIN_HOST = &quot;cdn.host&quot;; public static final String CDN_DOMAIN_PORT = &quot;cdn.port&quot;; public static final String CDN_DOMAIN_PATH = &quot;cdn.path&quot;; ... public static void contributeServiceOverride(MappedConfiguration&lt;Class, Object&gt; configuration, @Local HibernateSessionSource hibernateSessionSource) { configuration.add(HibernateSessionSource.class, hibernateSessionSource); // Servicio para usar un CDN lazy, pe. con Amazon CloudFront configuration.addInstance(AssetPathConverter.class, CDNAssetPathConverterImpl.class); if (isServidorJBoss(ContextListener.SERVLET_CONTEXT)) { configuration.add(ClasspathURLConverter.class, new WildFlyClasspathURLConverter()); } } public static void contributeApplicationDefaults(MappedConfiguration&lt;String, Object&gt; configuration) { ... configuration.add(CDN_DOMAIN_PROTOCOL, &quot;http&quot;); configuration.add(CDN_DOMAIN_HOST, &quot;s3-eu-west-1.amazonaws.com&quot;); configuration.add(CDN_DOMAIN_PORT, null); configuration.add(CDN_DOMAIN_PATH, &quot;cdn-plugintapestry&quot;); } ... }</code></pre></noscript>

Estás serían las URLs antiguas y nuevas con la implementación del AssetPathConverter:

Así de simple podemos cambiar el comportamiento de Tapestry y en este caso emplear un CDN, esta implementación es sencilla y suficiente pero perfectamente pordríamos implementarla con cualquier otra necesidad que tuviesemos. El cambio está localizado en una clase, son poco más que 46 líneas de código pero lo mejor es que es transparente para el código del resto de la aplicación, ¿que más se puede pedir?

Referencia:
Libro PlugIn Tapestry
Documentación sobre Apache Tapestry

http://tapestry.apache.org/current/apidocs/org/apache/tapestry5/services/AssetPathConverter.html
https://code.google.com/p/corner/source/browse/corner3/trunk/src/main/java/corner/asset/services/impl/CDNAssetPathConverterImpl.java
http://aws.amazon.com/es/cloudfront/dynamic-content/
http://stackoverflow.com/questions/16914673/serve-images-outside-web-application
https://issues.apache.org/jira/browse/TAP5-2201
http://apache-tapestry-mailing-list-archives.1045711.n5.nabble.com/T5-some-automagical-way-to-export-versioned-assets-to-CDN-td2399058.html
http://apache-tapestry-mailing-list-archives.1045711.n5.nabble.com/General-CDN-problem-that-came-up-with-Tree-component-td5671331.html
http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DownloadDistS3AndCustomOrigins.html
http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html

Author: "--"
Send by mail Print  Save  Delicious 
Date: Friday, 25 Jul 2014 14:52

Siendo el pedido más popular del wishlist de su comunidad, GOG.com provee soporte oficial para la plataforma GNU/Linux. GOG.com es una plataforma de distribución digital de videojuegos, un servicio para comprar y descargar juegos así como una comunidad de videojugadores.

GOG.com GNU/Linux

GOG.com GNU/Linux

Me hice una cuenta en GOG en 2011 porque había varios juegos clásicos disponibles a precios sumamente baratos que funcionaban bastante bien con Wine. Incluso llegué a comprar en una oferta Assasin’s Creed para PC y logré jugarlo de forma bastante decente sin tener una partición con Windows en mi computadora.

Volviendo a mi biblioteca, ya veo uno de los juegos disponible en mi sistema:

Gog.com biblioteca

Gog.com biblioteca

Tras la demanda de la comunidad, y seguramente con la presión de Valve con Steam para GNU/Linux y sus Steam Machines (computadoras con el sistema operativo SteamOS basado en GNU/Linux), GOG anunció hace un tiempo que también soportaría nuestro querido S.O. libre. Pensaban esperar a tener al menos 100 juegos para el lanzamiento. Pero teniendo 50 títulos -tanto clásicos como nuevos- prontos para distribución, infraestructura y un equipo de soporte técnico entrenado a la espera, decidieron publicarlo ya.

Cabe destacar que los juegos de GOG no tienen DRM, una ventaja respecto a Steam. Los juegos proveen dos opciones para descargar, un archivo tar.gz que debería funcionar en cualquier distro y paquetes .deb para las dos distros más populares entre los usuarios: Ubunty y Linux Mint. Van a soportar las versiones LTS actuales y futuras.

No solo van a distribuir videojuegos que ya tienen su port para Linux, también están trabajando en traer más juegos al sistema. Con motivo de su lanzamiento, tienen más de la mitad de estos juegos a un descuento del 75% de su precio.

Ya no quedan dudas ni falta de pruebas de que Linux es una plataforma viable para jugar. Esperemos que esto le traiga más popularidad y aportes al ecosistema del software libre.

Personalmente vengo jugando poco en la computadora, ya que volví a mi primer amor: Nintendo. El juego que más sigo jugando frecuentemente es Kingdom Rush en Steam. Pero sigo atento a nuevos lanzamientos en materia de juegos para PC para ver si alguno llama mi atención…

Author: "--"
Send by mail Print  Save  Delicious 
Date: Wednesday, 23 Jul 2014 23:53

Muy tarde, pero prefería no dejar que se unieran otras 2 semanas de retrospectiva semanal. Esta semana nada de saraos, más allá la boda de uno de los mejores desarrolladores que conozco (debo decir que fue un fiestón de los buenos :)).

Pero os cuento que al fin terminé con The Agile Samurai, libro que compré hace ya un buen puñado de meses y que he ido leyendo muy a ratos, es un libro ameno y que se lee rápido pero al no haber sido constante en su lectura me ha costado mucho más que a cualquier persona normal XD.

The Agile Samurai

Portada de The Agile Samurai

Es un libro que me hubiera aportado mucho más si lo hubiera leído hace 3 o 4 años. No es que me parezca un libro con mal contenido, ni mucho menos, simplemente que no me ha descubierto grandes cosas ni pienso que se meta mucho en detalle de los diferentes temas de los que trata. El libro se centra en cuestiones de equipos, clientes, conceptualización (hablando sobre agile inception)… y pasa un tanto de puntillas por el tema de ciertas prácticas técnicas. Os recomiendo un post de Yeray Darias que tiene más detalle sobre el contenido.

En mi opinión un libro que resume bastante bien muchas cuestiones agilistas principalmente de temas que consideraría algo más de gestión, y por ello creo que es ideal a modo de introducción al mundillo (desde luego que mucho mejor que muchos cursos de dudosa calidad que se imparten por ahí), además de para perfiles técnicos acostumbrados las prácticas técnicas para empezar a conocer prácticas de gestión.

No lo usaré como referencia, pero es posible que algún capítulo termine releyéndolo en algún momento para repasar algunas cosas que en mi día a día no suelen ser habituales.

Sobre los proyectos en marcha:

  • Estuve dándole una vuelta al rediseño de minchador, que lo teníamos parado. Así como empezar a plantear hacer algún tipo de demo a modo de documentación para enseñárselo a colaboradores y a algunos beta testers.
  • En Nort estuve trabajando en una aplicación para la integración con facebook para publicar ciertos contenidos. También tuvimos un pequeño code review para ponernos de acuerdo en algunos convenios para la organización del código, y a raíz de eso tocó hacer algo de refactoring, claro.
  • Solucioné algún bug de mhop y estuve empezando a trabajar en el flujo de aprobación de campañas.
  • También he empezado a replantearme mi idea de Biera Solutions, que fundé con idea de dedicar a hacer producto propio exclusivamente, para utilizar la marca también en el desarrollo a terceros (más allá de mi persona como freelance).

Buena semana

Author: "--"
Send by mail Print  Save  Delicious 
Date: Wednesday, 23 Jul 2014 07:02

¿Cuántas veces te pasa que, al tener implementada la solución a un problema, te das cuenta de que el problema era mucho más fácil de lo que parecía y que ahora podrías implementar una solución mucho más sencilla? ¿Cuánta de la complejidad de una solución es inherente al propio problema y cuánta se produce por la propia solución planteada?

La complejidad intrínseca de un problema es difícil de abordar. No la has creado tú. Tienes que convivir con ella. No puedes actuar sobre ella a corto plazo. Es verdad que con el paso de los años, van surgiendo técnicas que ayudan a disminuirla. Pero sólo con el paso de las décadas se dan mejoras significativas.

La complejidad accidental de un problema es mucho más fácil de abordar. La has creado tú (o tu organización, o tu cliente…). Puedes actuar sobre ella a corto plazo y disminuirla de manera significativa.

La buena noticia es que casi toda la complejidad de lo que hacemos es complejidad accidental. Ocurre en los procesos que nos autoimponemos, en los análisis y diseños que hacemos y en el código que escribimos. Tenemos la certeza de que:

  • normalmente cuando planteamos un problema no llegamos al fondo del POR QUÉ o PARA QUÉ se está abordando ese problema. Lidiamos muchas veces con los efectos del problema en lugar de ir a la causa.
  • siempre que se plantea un proceso ligero, en pocos pasos, éste tiende a complicarse con el tiempo. Cada vez que ocurre una excepción que el proceso no contempla, se añade una nueva fase al proceso sin tener en cuenta que la mayoría de las veces que se ejecute, al no darse esa excepción, esa fase será una pérdida de tiempo.
  • cuando programamos, planteamos una serie de abstracciones en función de nuestro entendimiento del problema. Cuando solucionamos ese problema, adquirimos un entendimiento del mismo que se puede adquirir de ninguna otra manera que no sea, precisamente, hacer el acto de solucionarlo. Ese entendimiento nos hace ver que las abstracciones que planteamos no iban a la esencia y por lo tanto son demasiado complejas.
  • el diseño de interfaces es especialmente clave en estos casos. Las interfaces normalmente reflejan los procesos de negocio. Si el proceso es complejo, la interfaz es compleja. Si la interfaz es compleja, la programación que haya por detrás también.

En desarrollos iterativos e incrementales, si de base planteamos procesos, interfaces y código complejos, la complejidad del problema lo único que hará es crecer de manera exponencial.

Si uno es consciente de esto, puede actuar sobre ello. Por eso es importante establecer mecanismos que busquen la simplicidad y la esencia de las cosas. Una lista no exhaustiva sería:

  • pregúntate POR QUÉ y PARA QUÉ 5 elevando a la 5 veces. Usa técnicas de análisis de causa raíz.
  • plantea siempre la solución más sencilla que se te ocurra. Iterativamente, intenta reducirla al absurdo y usa a expertos para que le tiren piedras. Si no se cae, es la solución a implementar.
  • antes de automatizar procesos, replantea los procesos. Si automatizas la mierda, tendrás mierda automática.
  • refactoriza el código. Refactorizar es entender que generamos complejidad accidental y que una vez vista la solución existe una manera de hacer las cosas más sencillas. También es entender que no simplificarlo ahora, implica generar más complejidad en el futuro.
Author: "--"
Send by mail Print  Save  Delicious 
Date: Tuesday, 22 Jul 2014 11:55
DebugHay veces que desde Javascript nos interesa ejecutar un código u otro en función de si la ejecución se está produciendo en un servidor de desarrollo o en uno de producción. Por ejemplo, en el primer caso suele ser interesante disponer de logs o herramientas de ayuda a la depuración, mientras que en el segundo lo normal es que queramos introducir código más eficiente y sin este tipo de condimentos.

En este post vamos a ver algunas técnicas muy básicas que nos permitirán ejecutar un código u otro en el lado cliente de aplicaciones ASP.NET MVC (o ASP.NET en general) en función del modo de compilación.

1. El lado servidor

En el lado servidor es bastante fácil de conseguir porque disponemos de directivas de compilación que nos permiten detectar si estamos generando los binarios en modo depuración:
// HomeController.cs
public ActionResult Index()
{
#if DEBUG
ViewBag.CompilationInDebugMode = true;
#else
ViewBag.CompilationInDebugMode = false;
#endif
return View();
}

@* Home/Index.cshtml *@
<p>Compilation in debug mode: @ViewBag.CompilationInDebugMode</p>
También podemos conocer en tiempo de ejecución si ASP.NET está compilando en dicho modo (compilation debug="true" en el web.config):
@* In a Razor view: *@
<p>ASP.NET debugging: @Context.IsDebuggingEnabled</p>
Normalmente esta última vía será la que utilicemos, pues es la que suele indicar si estamos ejecutando sobre un servidor de desarrollo o no, básicamente porque el cambio en el web.config del valor del parámetro debug a false es uno de esos pasos que nos han recomendado desde el principio de los tiempos al pasar una aplicación a producción. Y seguro que todos lo hacemos, ¿verdad? ;-)

2. ¿Y el lado cliente?

En el lado cliente, debido a la propia naturaleza dinámica del lenguaje Javascript, no existe una compilación en modo depuración; bueno, de hecho no existe ni siquiera compilación como tal ;-), por lo que tenemos que recurrir a algunos truquillos para conseguir emular este comportamiento.

2.1. Cargar scripts diferentes en función del modo de depuración de ASP.NET

Una posibilidad muy sencilla, y válida en algunos escenarios, es tener un archivo de script diferente para cada modo de compilación. De hecho, es empleada por muchas bibliotecas Javascript, que se distribuyen en forma de archivo .js para la versión de producción, normalmente ya compactada y minimizada, y .debug.js para la versión a utilizar durante el desarrollo.

Por ejemplo, supongamos que tenemos los dos siguientes archivos:
// File: Myscript.debug.js (development script)
function log(msg) {
console.log(msg);
}

// =====================================

// File: Myscript.js (production script)
function log(msg) {
// Nothing to do
}
Para cargar manualmente un archivo u otro en función del modo de compilación bastaría con hacer lo siguiente a la hora de referenciarlo:
<script src="http://www.planetacodigo.com/~/scripts/myscript@(Context.IsDebuggingEnabled? ".debug": "").js">
</script>
Sin embargo, si utilizamos bundles es aún más sencillo: de serie, el sistema de bundling ya distinguirá entre las distintas versiones de forma automática basándose en la extensión de los archivos. Por ejemplo, continuando con el caso anterior, al añadir un bundle como el siguiente el mismo componente de optimización seleccionará el archivo myscript.debug.js cuando la depuración de ASP.NET esté activada, y myscript.js en caso contrario:
bundles.Add(new ScriptBundle("~/bundles/myscript").Include(
"~/Scripts/myscript.*"
));
La referencia desde la vista en este caso sería así de simple:
@Scripts.Render("~/bundles/myscript")

2.2. Detección inline del modo de depuración

Pero no siempre el grano es tan grueso como en el apartado anterior. Muy frecuentemente no nos interesará sustituir completamente un script por otro en función del modo de compilación, sino conocer simplemente el modo en el que estamos para tomar una decisión a nivel de código. Esto nos permitiría escribir código como el siguiente:
function doSomething() {
if (DEBUG_MODE) {
log("Entering doSomething()");
}
// ... Code
if (DEBUG_MODE) {
log("Exiting doSomething()");
}
}
Por tanto, nuestro problema se reduce a establecer esa constante DEBUG_MODE a un valor que evalúe a cierto cuando estemos ejecutando sobre una aplicación compilada en modo depuración y a falso en caso contrario. Y aplicando lo visto anteriormente, podríamos conseguirlo de varias formas.

Una, realmente sencilla de implementar, consistiría en introducir un conciso bloque de scripts antes de cargar las bibliotecas o scripts que usen este valor, por ejemplo en el encabezado del layout o página maestra del sitio web, más o menos con lo siguiente:

@* Razor Layout *@
...
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My ASP.NET Application</title>
@Styles.Render("~/Content/css")
@Scripts.Render("~/bundles/modernizr")
<script>
window.DEBUG_MODE = @(Context.IsDebuggingEnabled? "true": "false");
</script>
</head>
...
Otra posibilidad sería establecer el valor de DEBUG_MODE en un script independiente del que tendríamos dos versiones, con extensiones .debug.js y .js, que respectivamente lo inicializarían a true y false, y usar cualquiera de las dos técnicas descritas anteriormente para cargar uno u otro en función del modo de compilación.

Publicado en Variable not found.
Author: "noreply@blogger.com (José M. Aguilar)"
Send by mail Print  Save  Delicious 
Date: Tuesday, 22 Jul 2014 07:16

IMG_1711

 

Ésta foto tiene ya un tiempo. Es de finales de 2013 (ya me cuesta publicar artículos, a ver si cojo un poco de inercia… :-P). En esa época a Guille y a mí nos cayó la primera versión de un potente software no-tan-big data, a hacer con AngularJS/Javascript, Elastic Search y AKKA, alrededor de un dominio que desconocíamos completamente y donde teníamos difícil encontrar a alguien que nos lo explicara, con una deadline inminente.

Si en un escenario aplica lo de typing is not the bottleneck, creo que es en éste. Con lo que decidimos que en lugar de estar levantándonos de nuestro sitio cada cinco minutos a preguntarnos cosas el uno al otro o tomar decisiones técnicas sin contrastar, íbamos a hacer todo el trabajo en pareja y favorecer el ancho de banda de la comunicación entre nosotros muy por encima del ancho de banda de tecleo.

En la foto se puede ver lo que para mí es un entorno ideal para trabajar el software:

  • un ordenador principal con dos teclados y dos ratones y espacio más que de sobra para estar cómodos y centrados frente a las pantallas.
  • un ordenador secundario. Esto es clave. En un escenario de incertidumbre máxima te atascas cada dos minutos y tienes que guguelear a mansalva. Si estás con un sólo ordenador, uno busca, y el otro tiene que andar aguantándose las ganas de buscar cosas distintas. Lo mejor es buscar en paralelo e ir comentando lo que se encuentra.
  • panel físico para gestionar el To Do. Según avanzábamos, íbamos redefiniendo una y otra vez el trabajo a hacer. Destruyendo post-its y creando unos nuevos con nuestra nueva visión de las tareas a realizar. Para nosotros era clave poder hacer esto sin los impedimentos de una herramienta electrónica como Jira, mucho más lenta. Máxime cuando la trazabilidad, en éste punto, no nos servía para nada.
  • pared pintable para discutir ideas y diagramar cosas. Tuvo mucho flow el poder discutir una idea, pintarla, e ir implementándola teniéndola siempre de referencia y pudiéndonos dar la vuelta en cualquier momento a discutir cualquier aspecto.

Es fácil enamorarse de ésta forma de trabajar. Y, al enamorarte, perder un poco la perspectiva. El sistema nos funcionó bien para su principal función: maximizar la información radiada y la comunicación entre nosotros como estrategia para abordar un producto de alta incertidumbre técnica y de dominio. Lo que no nos funcionó:

  • fuimos incapaces de mantener un sólo “to do” en el panel. Como éramos novatos en todo, aprendíamos una manera más decente de hacer las cosas cada media hora, y todo el código anterior nos parecía refactorizable (y lo apuntábamos al “to do”). Al final manteníamos el panel, un archivo todo.txt en el intellij, y las anotaciones //TODO:  en el propio código. Aunque planteábamos sesiones dedicadas sólo a fundirnos esas listas, no lo conseguíamos.
  • hacer pairing el 100% del tiempo trabajando a veces en jornadas de 10 horas es demasiado. Ya sé que esto, leído, puede sonar bastante evidente. Pero cuando estás dándolo todo, cansado, hay cosas de las que no te das cuenta. Por ejemplo, yo adquirí muchos conocimientos de “angularjs pasivo“. Como Guille es más rápido que yo pensando y empezó el proyecto manejando más de AngularJS, yo solía ser un mero espectador cuando encontrábamos una solución a un problema en JS. No soy manco: a veces se me ocurría la solución a mí… pero normalmente (especialmente cuando estábamos cansados) la implementaba él porque le suponía menos esfuerzo. Con lo que yo no terminaba de asimilar las cosas.

Lecciones aprendidas. Cómo y cuando hacemos pairing ahora:

  • hacemos pairing siempre que lo que nos impide avanzar es el conocimiento (ya sea técnico o de dominio).
  • hacemos pairing siempre que se va a tomar una decisión de diseño importante en la que se va a basar el proyecto (cómo vamos a organizar los componentes, si se va a usar tal o cuál patrón, estrategia de testeo, etc…)
  • siempre que hacemos pairing tenemos una lista preparada con el orden del día. Y sí: normalmente hacemos pairing de jornadas completas, preparando el entorno previamente para ser productivos y tener toda la información disponible y la posibilidad de tener discusiones productivas.
  • es importante no hacer pairing cuando quieres asimilar algo que has aprendido.

Como consecuencia de todo esto, en el último proyecto que hemos hecho juntos habremos hecho un día a la semana de trabajo en pareja. Al menos durante la fase principal del proyecto, acumulando en ese día las decisiones de diseño y la lista de cosas que queríamos aprender del otro. Inventándome la cifra, diré que habremos hecho un 10-15% de pairing.

¿100% o 15% de pairing? Para mí aquí ( como siempre) la clave es el contexto. Y el punto de vista desde el que analizar ese contexto, es el del conocimiento. Si analizo el proyecto en el que hicimos 100% de trabajo en pareja y soy sincero conmigo mismo, no creo que hubiéramos acabado antes ni mejor haciéndolo por separado. No creo que hubiera salido más barato ( argumento clásico en contra del trabajo en pareja). Sin embargo, si analizo el proyecto donde hemos hecho 10-15% de pairing, si que creo que más tiempo hubiera supuesto perder el tiempo. Y también sé que trabajar más por mi cuenta en AngularJS me ha hecho mucho más ágil e independiente.

 

Author: "--"
Send by mail Print  Save  Delicious 
Date: Monday, 21 Jul 2014 14:26
Enlaces interesantesAhí van los enlaces recopilados durante la semana pasada, espero que os resulten interesantes ;-)

.Net

ASP.NET

Azure / Cloud

Conceptos/Patrones/Buenas prácticas

Data access

Html/Css/Javascript

Visual Studio/Complementos/Herramientas

Cross-platform

Otros

Publicado en Variable not found
Author: "noreply@blogger.com (José M. Aguilar)"
Send by mail Print  Save  Delicious 
Date: Monday, 21 Jul 2014 07:39

Este mes se cumple el tercer aniversario de este pequeño proyecto personal. Son tres años en los que he escrito un montón de tonterías (222, concretamente), he conocido a mucha gente, he discutido con casi todos los que se han dejado y, sobre todo, he aprendido mucho.

¿Por qué?

Al hilo de un reciente encuentro virtual entre varios autores de blogs, Rubén Fernández escribía en su blog “¿y por qué tengo un blog?“.

Hay muchos motivos para escribir un blog. Hay quien busca crear una marca personal, hacer marketing de contenidos para otros, o incluso intentar ganar algún premio, pero la mejor motivación que puedes encontrar es disfrutar escribiéndolo. Si no disfrutase escribiendo, estoy seguro de que no sería capaz de sacar un hueco todas las semanas para escribir un nuevo post.

Es verdad que mantener un blog ayuda a aprender cosas nuevas, a conocer gente interesante, a potenciar una “marca personal” (para los que les preocupe) y a unas cuantas cosas más. Esos factores pueden ser lo que te hagan a dar el paso para empezar a escribir un blog, pero cuando llevas un tiempo haciéndolo, es fundamental que disfrutes con ello.

Eso de escribir para disfrutar implica también escribir para uno mismo, y por eso cada vez es más fácil encontrar en este blog posts que no interesan a nadie (bueno, o a casi nadie), pero, sinceramente, disfruto más escribiendo eso que un tutorial sobre la tecnología de moda que tenga 500 veces más visitas (y hablo con conocimiento de causa ;-))

…y ¡vacaciones!

No se me ocurre mejor forma de celebrar este tercer aniversario que tomarme unas semanitas de descanso. Este año se me ha hecho muy largo, tanto personal como profesionalmente, y ya me va tocando desconectar (un poco, que si no me aburro), relajarme, y resolver algunos asunto más mundanos que llevo posponiendo tiempo, así que, por lo menos hasta mediados de agosto, no creo que publique mucho.

De todas formas, como se suele decir, la cabra tira al monte y seguro que durante este tiempo seguiremos discutiendo de vez en cuando en twitter.

¡Felices vaciones a todos!

lago_enol

Imagen de Dominik Morbitzer en flickr

No hay posts relacionados.

Author: "--"
Send by mail Print  Save  Delicious 
Date: Monday, 21 Jul 2014 06:37

Éste es un pequeño apunte para recordar un panel que nos ha funcionado muy bien a Guille y a mí en un desarrollo que hemos hecho a presupuesto cerrado.

la foto

 

El panel es una especie de Story Map traspuesto. Cada columna es una entrega (diseñamos una estrategia con dos demos y una entrega final). Cada fila es un vertical de la aplicación (un “context boundary” que se diría en DDD).  La primera celda de cada fila contiene un glosario de términos del vertical y una lista de dudas o incertidumbre a ir aclarando.

No dimos color a los post-it´s. Distinguimos la funcionalidad del spike o tarea técnica poniendo de título al post-it “spike”. Viéndolo en retrospectiva hubiera estado bien darles un color de post-it distinto. Lo que si distinguimos fue el feedback/detalle descubierto en las demos(post-it amarillo), de la funcionalidad original (verde). Para visualizar si una funcionalidad estaba terminada, simplemente la tachábamos.

Para visualizar las horas que habíamos empleado (clave en éste proyecto a precio cerrado) usamos el post-it fucsia. Cada semana actualizábamos las horas, moviendo el post-it hacia la derecha proporcionalmente. De tal manera que era fácil ver si estábamos en el último tercio de horas y todavía en el segundo tercio de funcionalidades (vamos tarde), etc…

En definitiva, nos ha funcionado muy bien para controlar el release plan y controlar el tema económico. También para gestionar la incertidumbre del proyecto y el detalle de las funcionalidades.

Creo que éste panel no funcionaría en cualquier contexto. El panel obedece a un escenario donde no hay bugs (porque todo el trabajo diseñado es anterior a que se ponga en producción de verdad el software) y donde al estar dos desarrolladores es fácil saber quién está haciendo qué.

 

Author: "--"
Send by mail Print  Save  Delicious 
Date: Saturday, 19 Jul 2014 19:00
Apache Tapestry

Como he comentado en un artículo anterior sobre los modelos push y pull empleados en los motores de plantillas donde comentaba algunas diferencias entre ellos, en el modelo pull es la vista la que pide los datos al controlador y no el controlador el que proporciona los datos a la vista como se hace en el modelo push. Un problema que puede plantear el que la vista pida los datos al controlador es que si la devolución de los datos solicitados son costosos en tiempo del cálculo, carga para el sistema en CPU o memoria, o intensivos en entrada/salida de disco o red y se piden varias veces puede suponer como resultado que el tiempo empleado para generar la página sea elevado o la aplicación consuma recursos innecesarios.

Apache Tapestry que emplea el modelo pull dispone de la anotación Cached que permite cachear el resultado de un método a nivel de componente y página durante la generación de la misma. Su uso sería el siguiente:

<noscript><pre><code>package info.blogstack.pages; import info.blogstack.entities.Post; import info.blogstack.entities.Source; import info.blogstack.misc.Globals; import info.blogstack.misc.Utils; import info.blogstack.services.MainService; import info.blogstack.services.dao.Direction; import info.blogstack.services.dao.Pagination; import info.blogstack.services.dao.Sort; import java.util.ArrayList; import java.util.List; import org.apache.tapestry5.annotations.Cached; import org.apache.tapestry5.annotations.Property; import org.apache.tapestry5.ioc.annotations.Inject; public class Label { private Object[] context; @Property private Integer page; @Inject private MainService service; @Property private Source source; @Property private info.blogstack.entities.Label label; @Property private Post post; void onActivate(Object[] context) { this.context = context; } Object[] onPassivate() { return context; } void setupRender() { String hash = Utils.getHash(new Object[] { context[0] } ); label = service.getLabelDAO().findByHash(hash); page = (context.length &gt;= 3) ? (Integer) context[2] : null; page = (page == null) ? 0 : page; } /** * Método que devuelve las articulos publicadas o actualizadas más recientemente de una etiqueta. */ public List&lt;Post&gt; getPosts() { List&lt;Sort&gt; sorts = new ArrayList&lt;&gt;(); sorts.add(new Sort(&quot;date&quot;, Direction.DESCENDING)); Pagination pagination = new Pagination(Globals.NUMBER_POSTS_PAGE * page, Globals.NUMBER_POSTS_PAGE * (page + 1), sorts); return service.getPostDAO().findAllByLabel(label, pagination); } @Cached(watch = &quot;label&quot;) public Long getPostsCount() { return service.getPostDAO().countBy(label); } public boolean isFirstPage() { return page == 0; } public boolean isLastPage() { return (page + 1 &gt; Globals.NUMBER_PAGES_LABEL || getPostsCount() &lt;= Globals.NUMBER_POSTS_PAGE * (page + 1)); } public Object[] getPreviusContext() { return (page - 1 &lt;= 0) ? new Object[] { label.getName() } : new Object[] { label.getName(), &quot;page&quot;, page - 1 }; } public Object[] getNextContext() { return (page + 1 &gt; Globals.NUMBER_PAGES_LABEL) ? new Object[] { label.getName() } : new Object[] { label.getName(), &quot;page&quot;, page + 1 }; } }</code></pre></noscript>

En este ejemplo cada vez que se llama a los métodos getPosts, getPostsCount se accede a una base de datos (o sistema externo) que lanza una consulta, supongamos, costosa de calcular o que simplemente es innecesaria hacerla varias veces. Usando la anotación Cached podemos hacer la aplicación más eficiente evitando las segundas llamadas a los métodos. Si el componente Label del ejemplo se usa dentro de un bucle de un componente loop y como parámetros se le van pasando varios labels las llamadas a los métodos getPosts y getPostCount se realizarán solo para cada valor diferente.

Algunas veces puede interesarnos que el cacheo dependa de un dato, es decir, que para cada valor de un dato la anotación Cached devuelva diferentes resultados. Y esto es lo que se hace en el ejemplo con el parámetro watch de la anotación, por cada valor de la propiedad label el resultado probablemente sea diferente pero nos interesa que el método solo se ejecute una vez por cada diferente valor, dado que los artículos y el número de ellos únicamente variarán en función de esta propiedad. Esto también puede ser usado para que solo se evalúe los métodos una vez por iteración de un bucle estableciendo la expresión watch al índice del bucle.

<noscript><pre><code>&lt;html t:type=&quot;layout&quot; xmlns:t=&quot;http://tapestry.apache.org/schema/tapestry_5_4.xsd&quot; xmlns:p=&quot;tapestry:parameter&quot;&gt; &lt;t:postcomponent post=&quot;post&quot; excerpt=&quot;true&quot;/&gt; &lt;t:remove&gt; Llamadas ádicionales para obtener los artículos, devolverán los mismos datos. &lt;/t:remove&gt; &lt;t:postcomponent post=&quot;post&quot; excerpt=&quot;true&quot;/&gt; &lt;t:postcomponent post=&quot;post&quot; excerpt=&quot;true&quot;/&gt; &lt;/html&gt;</code></pre></noscript>

Aún así, la anotación Cached funciona a nivel de petición, cada vez que que se haga una petición a la aplicación y se llame al método anotado por primera vez y por cada valor de la expresión watch se ejecutará el método. Si tenemos muchas peticiones o un determinado componente tarda mucho en generar su contenido, por ejemplo, porque depende de un sistema externo lento (base de datos, http, …) quizá lo que debamos hacer es un componente que almacene durante un tiempo el contenido que genera y sea devuelto en múltiples peticiones, de modo que evitemos emplear un tiempo costoso en cada petición. Para ello, podríamos desarrollar un componente que usase una librería de cahce como por ejemplo EHCache.

Referencia:
Libro PlugIn Tapestry
Documentación sobre Apache Tapestry

Author: "--"
Send by mail Print  Save  Delicious 
Date: Thursday, 17 Jul 2014 06:38

Siguiendo un poco con la importancia de distinguir entre simple, complejo, complicado, etc… Es muy interesante el marco de decisión( o de análisis) Cynefin.

Cynefin_framework_Feb_2011

¿Cómo tomar decisiones o analizar un problema?

  • Si existe una relación clara entre causa y efecto, el escenario es simple. Puedes percibir la situación, categorizarla y, en función de la categoría, responder. Existen unas recetillas que te dicen en función de la categorización cuál es la mejor respuesta (best practices).
  • Si existe una relación entre causa y efecto, pero no está clara, el escenario es complicado. Requiere experiencia y análisis antes de actuar. Pueden existir unas buenas prácticas pero no una sola manera de hacer las cosas bien.
  • Si existe una relación entre causa y efecto, pero sólo eres capaz de establecerla a posteriori y, además, las consecuencias de lo que está ocurriendo no son predecibles, el escenario en el que te encuentras es complejo. La situación requiere probar varias cosas, coger feedback y actuar. La manera buena de hacer las cosas va emergiendo en función del feedback. No es conocida a priori.
  • Si no existe ninguna relación entre causa y efecto, ni siquiera analizando las cosas a posteriori, estás en una situación caótica. Probar para obtener feedback no sirve porque al repetir no necesariamente va a ocurrir lo mismo. La única posibilidad es dar un paso al frente, actuar y ver qué pasa. Aquí eres siempre un novato y no puedes extraer buenas prácticas en función de la experiencia.

Creo que en general nos movemos entre lo complejo y lo complicado. Y que es difícil saber cuando pararte a analizar bien las cosas y cuando no analizarlas tanto e intentar hacer algo para tener feedback rápido. Me gusta la manera en la que Cynefin enmarca todo esto.

Para un análisis muy bueno sobre Cynefin y Agilismo el post de Liz Keogh.. y ¡ojo a la respuesta de Jon Reffies!

 

Author: "--"
Send by mail Print  Save  Delicious 
Date: Wednesday, 16 Jul 2014 11:24

Estimados usuarios de Xailer,

La futura versión de Xailer incorporará una nueva funcionalidad que creemos puede ser de gran utilidad para todos vosotros y de la cual estamos muy satisfechos por el resultado obtenido, que es la creación automática de controles de usuario.

Actualmente Xailer ya permite la creación de controles de usuario que se pueden utilizar posteriormente en el IDE y en sus aplicaciones finales. De hecho el proyecto \Samples\Controls\Samples.xpj es un claro ejemplo del mismo. Dicho ejemplo crea todos los controles que puede encontrar en la pestaña ‘Samples‘ de la barra de controles. Para su utilización tan sólo deberá incluir en sus proyectos la librería \Xailer\lib\Samples. No obstante, muy pocos usuarios se han adentrado en la creación de controles propios, seguramente por creer que se trata de un proceso complicado y tedioso.

Para facilitaros las cosas un poquito más la nueva versión de Xailer incorporará un completo sistema de creación de controles automático en base a cualquier control que tengáis de cualquier formulario. La idea es que todas las propiedades que hayáis indicado en dicho control, junto con los recursos que utilice, se conviertan en un control adicional que se pueda utilizar en cualquier proyecto actual o futuro.

El funcionamiento es muy sencillo: Seleccionamos primeramente el control personalizado que deseamos crear como control propio, pulsamos el botón contextual del mismo y seleccionamos la opción de ‘Creación de control de usuario…‘. A continuación se nos mostrará el siguiente diálogo:

UserControlDialog

Tan sólo deberá indicar el nombre de la nueva clase, el tooltip que se le mostrará cuando lo pre-seleccione en el IDE, la imagen que se mostrará en la barra de controles y el nombre de la pestaña donde se mostrará el nuevo control. Incluso puede dejar todos los valores por defecto si lo desea.

El IDE creará, si no existiese, un nuevo proyecto de nombre ‘MyControls‘ en su carpeta de ‘Archivos de usuario\Componentes‘ (ver opciones generales) e irá introduciendo todos los controles de usuario que vaya creando. Dicho proyecto será el responsable de crear la DLL MyControls.dll para el IDE y de las librerias para su uso bien por MinGW o Borland C++. Observe como en el diálogo de creación de control usuario existe una casilla para abrir este nuevo proyecto de forma inmediata.

Para poder empezar a utilizar sus nuevos controles deberá cargar en el IDE la librería MyControls. DLL utilizando la opción de menú Componentes->Gestor de componentes.

UserControlGestorComponentes

El IDE de Xailer controla de forma automática los recursos que se requieran. Cuando se crea el control de usuario, sus recursos son copiados al proyecto ‘MyControls‘ y cuando el control es utilizado en cualquier otro proyecto sus recursos son a su vez copiados desde el proyecto ‘MyControls‘ a su proyecto de forma completamente transparente.

Esperamos que con esta nueva funcionalidad ya no tengáis motivos para no crear vuestros propios controles. ;-) Cualquier comentario al respecto será bienvenido.

Un cordial saludo

Author: "--"
Send by mail Print  Save  Delicious 
Date: Tuesday, 15 Jul 2014 16:13

Cosas de ir liado de trabajo, tener un puñado de otros compromisos y una parte de pereza para ponerme a escribir cuando tenía un rato; se me ha vuelto a juntar una semana sin publicar la retro semanal.

En estos 15 días: inauguración de piso, Geek’s Talks, Slap!, cierre del trimestre anterior, consultobirra con los amigos de Certicalia, boda de amigos, gestionar algún tema de viejos proyectos, el UXZaragoza

A destacar el UXZaragoza, claro. Fueron 4 charlas que creo que se complementaron muy bien, y me pareció interesante que en cada una de ellas se hablara desde las experiencias propias.

Adriano y Pablo hablaron de cómo han ido integrando técnicas de UX con las de desarrollo ágil, cosas que les han ido funcionando mejor y peor durante los 4 meses que llevan trabajando en spines: Las cosicas del Lean UX.

Guillermo hizo una charla en la que habló más que nada sobre experimentar (o zancochar :)), probar herramientas, técnicas… que quizás no se vayan a usar a corto plazo pero de las que quizás podamos sacar partido en el futuro: La locura, esa facultad fundamental.

Daniel expuso como trabajos de consultoría que a priori son de diseño o UX de producto digital, terminan teniendo impacto en el mundo físico. Básicamente en cambiar flujos de trabajo que ayudan a mejorar la experiencia de usuario del cliente: Service design de pueblo.

Verónica nos estuvo hablando de tests con usuarios de laboratorio, del modo de observación, de análisis cualitativos y cuantitativos, de analizar los resultados, etc: Falla rápido y barato, falla investigando.

Podéis ver la actividad en twitter con #uxzaragoza.

Y acerca de los trabajos en marcha:

  • Vuelta a dedicar algún tiempo a gestiones para proyectoSinNombre (un día igual lo cerramos y todo…).
  • Empecé ya con Nort, nuevo proyecto al que mayor dedicación le voy a dar durante los próximos meses. Entro a trabajar a modo de contractor con un equipo de varias personas con quienes estaremos desarrollando la aplicación usando Grails. Sobre el cliente y la temática del proyecto no puedo hablar, al menos por el momento.

    Ya hemos hecho el primer sprint, empezamos a implementar historias de usuario, hemos hecho la primera demo, la primera retro… y ver que, evidentemente, tenemos muchos puntos de mejora.

  • He hecho algunas cosas en mhop. Le he dedicado menos disponibilidad, pequeños detalles y algunas reuniones sobre el camino a seguir pensando en el medio plazo.

Buena semana.

Author: "--"
Send by mail Print  Save  Delicious 
Date: Tuesday, 15 Jul 2014 15:18
image[2]La revisión 2.1 de SignalR ha incluido otra característica que puede ser interesante en determinados escenarios: la posibilidad de notificar al cliente del progreso de la ejecución de una acción invocada en el hub.

Aunque en los métodos que normalmente implementamos no tiene demasiada utilidad, sí puede ayudar a mejorar la interacción y experiencia de usuario cuando se trate de ejecutar métodos costosos como el siguiente:
public class GodsOfTheForest: Hub
{
[...]
public async Task<int> GetTheAnswerToLifeTheUniverseAndEverything()
{
for (var i = 0; i < 10; i++)
{
await Task.Delay(1000);
// Do something very important here
  }
return 42;
}
}
Hasta ahora, si desde el cliente llamábamos a este método del hub, sólo podíamos tomar el control cuando la llamaba finalizaba, bien obteniendo el resultado si todo había ido bien, o bien capturando el error producido. Por ejemplo, en Javascript lo conseguíamos con un código como el siguiente:
proxy.server.getTheAnswerToLifeTheUniverseAndEverything()
.done(function (result) {
console.log("The answer is: " + result);
})
.fail(function(err) {
console.log("Oops, something went wrong: " + err.message);
});

Notificación de progreso en SignalR 2.1

Para notificar el progreso de la ejecución en un método del hub, a partir de SignalR 2.1 tenemos que hacer básicamente dos cosas:
  • En primer lugar, añadir al método del hub un parámetro de entrada adicional de tipo IProgress<T>, que obligatoriamente debe ser el último del método. T es el tipo de dato que vayamos a utilizar para notificar el progreso al cliente; por ejemplo, si vamos a notificar un porcentaje, podríamos usar Progress<int>.
  • En segundo lugar, en el punto del código desde donde queremos realizar la notificación, invocar al método Report() del parámetro anterior suministrándole el valor de progreso deseado.
Pero seguro que queda más claro si lo vemos con un poco de código basándonos en el ejemplo anterior:
public async Task<int> GetTheAnswerToLifeTheUniverseAndEverything(
IProgress<int> progress)
{
for (var i = 1; i <= 100; i++)
{
await Task.Delay(1000);
progress.Report(i);
}
return 42;
}
De esta forma tan simple estaremos enviando al cliente el progreso de la operación :-) Por supuesto, podríamos enviar al cliente notificaciones de cualquier tipo, como en el siguiente ejemplo, donde usamos IProgress<string> para poder notificar con mensajes de texto:
public async Task<int> GetTheAnswerToLifeTheUniverseAndEverything(
IProgress<string> progress)
{
progress.Report("Just starting...");
await Task.Delay(1000);
progress.Report("Working hard...");
await Task.Delay(1000);
progress.Report("Almost done...");
await Task.Delay(1000);
progress.Report("One second to finish...");
await Task.Delay(1000);
progress.Report("Finished");
return 42;
}

Consumo desde clientes del hub

El consumo de los datos de progreso sería bastante simple desde nuestro cliente Javascript, simplemente usar este método progress() para especificar la función callback que recibirá los datos de progreso:
proxy.server.getTheAnswerToLifeTheUniverseAndEverything()
.progress(function (value) {
log("Progress: " + value + "%");
})
.done(function (result) {
log("The answer is: " + result);
});
Ah, y un detalle muy importante a este respecto: el uso de esta característica requiere la versión 1.7 o superior de jQuery, más que nada porque en el objeto Deferred que retornan las llamadas a las funciones del hub antes no existía el método progress() que estamos usando para recibir las notificaciones de progreso.

Desde clientes .NET es igualmente sencillo. Se han añadido sobrecargas al método Invoke() de forma que podemos indicar el código a ejecutar al recibir el progreso de la operación. El siguiente código muestra una llamada a un método que retorna un entero y cómo gestionar las notificaciones de progreso que nos llegan también en forma de número entero:
int result = await proxy.Invoke<int, int>(
"GetTheAnswerToLifeTheUniverseAndEverything",
(int progress) =>
Debug.WriteLine("Progress: " + progress + "%")
);
El primer parámetro genérico del método Invoke()es el tipo de datos que retornará la llamada al hub, como ha sido siempre hasta ahora, mientras que el segundo es el utilizado para notificar el progreso. Así, si los mensajes de progreso nos llegan en forma de cadena de caracteres, deberíamos usar:
int result = await proxy.Invoke<int, string>(
"GetTheAnswerToLifeTheUniverseAndEverything",
(string progress) => Debug.WriteLine(progress)
);
Bueno, pues con este post podemos dar por cerrada la serie sobre las novedades de SignalR 2.1. Hay algunas más, pero creo que no son tan interesantes como las que hemos ido viendo; si tenéis interés en conocerlas, podéis acudir al repositorio de Github de SignalR.

Publicado en Variable not found.
Author: "noreply@blogger.com (José M. Aguilar)"
Send by mail Print  Save  Delicious 
Date: Monday, 14 Jul 2014 11:45
Enlaces interesantesAhí van los enlaces recopilados durante la semana pasada, espero que os resulten interesantes ;-)

.Net

ASP.NET

Conceptos/Patrones/Buenas prácticas

Data access

Html/Css/Javascript

Visual Studio/Complementos/Herramientas

Otros

Publicado en Variable not found
Author: "noreply@blogger.com (José M. Aguilar)"
Send by mail Print  Save  Delicious 
Date: Monday, 14 Jul 2014 05:06

Hace no tanto tiempo (aunque en la escala de internet pueda parecer siglos) gestionar las dependencias de un proyecto era algo relativamente complicado o, al menos, laborioso.

En aquellos lejanos y oscuros tiempos, para incluir en tu proyecto, por ejemplo, NHibernate, debías buscar la página de descarga correspondiente, elegir la versión correcta para tu versión de .NET, buscar posibles dependencias (log4net, Castle.DynamicProxy, etc.), cruzar los dedos para que estuvieran en el zip que te habías descargado de la página de NHibernate y, si no estaban, empezar a buscarlos en sus respectivas páginas de descarga, asegurándote de bajar las versiones exactas que necesitabas para NHibernate y rezando para que no hubiera colisiones entre unas versiones y otras, especialmente si había algún assembly firmado (recuerdo con especial “cariño” log4net y sus versiones 1.2.9 y 1.2.10). Finalmente copiabas todo con mucho cuidado en una carpeta .\lib al lado de los fuentes de tu proyecto y lo añadías a tu sistema de control de código fuente (subversion si tenías suerte, Visual Source Safe si no). Vamos, todo muy rápido, divertido y agradable.

Por suerte hoy en día prácticamente todas las plataformmas de desarrollo cuentan con gestores de paquetes para realizar esta gestión de dependencias y, lo que es más importante, se han convertido en la manera “oficial” y habitual de instalarlas. En realidad todo esto de los gestores de paquetes no es algo nuevo, existen desde hace mucho tiempo, pero en algunas plataformas como .NET su popularidad es relativamente reciente (que Microsoft publicase NuGet fue clave en una comunidad que había ignorado antes intentos como OpenWrap o Horn).

Utilizando un gestor de paquetes, todo el proceso de descarga e instalación de una dependencia que describía antes se reduce a algo como:

npm install underscore

Así de simple. Con sólo un comando el gestor de paquetes se conectará a un servidor que contiene un índice con los paquetes publicados y se descargará el paquete seleccionado, ejecutará los scripts de instalación adecuados y hará lo mismo recursivamente con las dependencias del paquete indicado.

Además, estos paquetes que has instalado quedan registrados en un fichero de configuración asociado al proyecto, por lo que existe un punto centralizado en el que revisar qué dependencias tienes instaladas y con qué versiones.

Visto así, es innegable que los gestores de paquetes suponen un gran avance para el desarrollo de software facilitando la gestión de dependencias y seguramente sean uno de los motivos por los cuales hoy en día se utilizan cada vez más componentes externos (muchos de ellos open source) en todo tipo de proyectos.

Cuidado con las dependencias que asumes

Desgraciadamente esta facilidad de uso también tiene puede tener consecuencias negativas y es importante tenerlas en cuenta.

Al ser tan sencillo añadir nuevas dependencias a un proyecto, es habitual encontrar proyectos con decenas de dependencias externas, cada una de ellas con sus propias dependencias. Esto no es algo malo en si mismo, a fin de cuentas, ¿para qué reinventar la rueda?, pero no hay que olvidar que cada dependencia que asumes en un proyecto es una dependencia que tienes que mantener.

Es tan fácil añadir un nuevo paquete que a veces, en lugar de implementar una función de 10 líneas de código, se acaba asumiendo una dependencia sobre una librería de miles de líneas de código para usar sólo esa función. El utilizar una librería externa, probada y ampliamente usada proporciona una sensación de seguridad que no siempre es real y, cuando se produce un problema, bien porque hay un bug en esa librería o porque hay problemas de compatibilidad entre dos librerías que estamos utilizando, depurarlo puede ser bastante complicado.

Cuando aparece un problema, no sirve de nada decir “el problema no está en mi código sino en la versión 1.9.10 de jQuery, yo no tengo la culpa”. De cara a tus usuarios y tus clientes, lo que no funciona es tu aplicacion, esa por la que ellos han pagado. Al final tienes que responder no sólo por tu código sino por el de todas las dependencias que estás utilizando.

Esto no quiere decir que no haya que utilizar librerías externas. Igual que no tiene mucho sentido ponerte a escribir tu propio servidor web y es más razonable usar IIS, Apache o lo que más te guste, no merece la pena invertir tiempo en escribir tu propio ORM cuando hay alternativas perfectamente válidas.

Lo que sí es necesario es ser consciente de que cada dependencia que asumes tiene un precio. Cuando programamos, si vemos una clase que recibe 12 parámetros en el constructor lo consideramos como algo problemático (o al menos algo que requiere cierta justificación), porque sabemos que esa clase tiene demasiadas dependencias, lo que aumenta su acoplamiento con el resto del sistema y complica su futuro mantenimento. Pues con las dependencias de los proyectos pasa algo parecido. Si un proyecto depende de 12 paquetes distintos, estamos introduciendo 12 puntos de problemas potenciales, por lo que más nos vale ser conscientes de ellos y tener un buen motivo para hacerlo.

Recuperación automática de paquetes

Todos los gestores de paquetes que conozco cuentan con alguna opción para volver a descargarse los paquetes desde un repositorio central. Si recordáis el flujo de trabajo que describía al principio del post, antes era frecuente almacenar los paquetes en alguna carpeta de proyecto e incluirlos en el control de código fuente junto con nuestro propio código. Hoy en día, esto no es lo habitual e incluso se considera una mala práctica, puesto que podemos incluir únicamente un fichero de configuración y descargar los paquetes bajo demanda de sus servidores de origen cuando los necesitemos.

La principal ventaja de esto es que el tamaño de nuestro repositorio de código será mucho menor, especialmente si los paquetes se distribuyen de forma binaria (como es habitual en .NET o Java), que es más difícil de comprimir y de almacenar usando deltas cuando hay cambios de versión.

En un mundo en el que el almacenamiento en disco es tan barato puede parecer que esto no es importante, pero cuando pensamos en el tráfico de red al clonar un repositorio, especialmente con sistemas de control de versiones distribuidos como Git o Mercurial en los que se clona el repositorio completo (y no sólo una versión), y en accesos a través de conexiones a internet en lugar de redes locales, sí que es un factor a tener en cuenta.

Como siempre, esto no es gratis y utilizar la recuperación automática de paquetes en lugar de almacenar las dependencias en el control de código fuente también tiene sus inconvenientes. Ahora, para poder compilar nuestro proyecto, dependemos de que el repositorio de paquetes esté disponible. Por suerte no es muy habitual que estos repositorios centrales se caigan, pero tampoco es imposible.

Esto se puede resolver creando un repositorio de paquetes propio, dentro de tu red local, con una copia de los paquetes que usas en tus proyectos. Es una buena opción, especialmente en organizaciones grandes donde compensa aprovechar las ventajas de no incluir las dependencias en el control de código fuente y se necesitan unas ciertas garantías de disponibilidad, pero montar estos servidores requiere algo de tiempo y es una pieza más del sistema a mantener.

Personalmente, cuando estoy desarrollando un proyecto profesionalmente, siempre incluyo todas las dependencias en el control de código fuente. En mi escenario concreto, con todos los desarrolladores trabajando en una misma red local, no me compensa el riesgo de tener que lidiar con un servidor externo caído o un paquete eliminado para ahorrarme 10 segundos al clonar un repositorio (operación que, por otra parte, tampoco es muy frecuente en nuestro caso).

Conclusiones

Los gestores de paquetes son un gran invento para facilitarnos las cosas al desarrollar aplicaciones porque nos permiten gestionar las dependencias de una manera fácil y cómoda, pero eso no debe hacernos olvidar que cada paquete que instalamos es una dependencia de nuestra aplicación por la que deberemos responder en el futuro.

La existencia de servidores centralizados desde los que descargar las dependencias simplifica el acceso a las mismas y puedo ayudarnos a reducir el almacenamiento en disco y el tráfico de red, aunque a cambio estaremos introduciendo una dependencia entre nuestros procesos y la disponibilidad de esos servidores, por lo que será necesario valorar si realmente merece la pena.

No hay posts relacionados.

Author: "--"
Send by mail Print  Save  Delicious 
Date: Friday, 11 Jul 2014 18:54
Apache Tapestry

Una página web está formada por un conjunto de páginas enlazadas entre ellas. Cada página está formado por un html diferente pero normalmente todas las páginas de una misma web comparten el mismo aspecto variando solo una sección donde está el contenido propio de la página. La cabecera de la página, el pie de la página o los menús de navegación suelen estar presentes en todas las páginas de la web y suelen ser los mismos.

En este artículo voy a explicar como crear un componente que nos de a todas las páginas un aspecto común de una aplicación usando apache Tapestry como framework web de tal forma que esa parte común no esté duplicada en la aplicación y pueda ser reutilizada fácilmente. En el caso de Blog Stack las páginas se componen de las siguientes partes.

El esquema de la plantilla será una cabecera, una barra de navegación con enlaces a diferentes secciones de la web, un menú lateral con contenido variable según la página, el contenido que variará según la página y un pie de página. Como todo componente de Apache Tapestry está formado de una clase Java y una plantilla. El componente puede tener diferentes parámetros, y en el caso del de la plantilla muchos para poder variar el contenido por defecto de las diferentes secciones de la página, estos son aside1, aside2, aside3, aside4.

<noscript><pre><code>package info.blogstack.components; import info.blogstack.entities.Adsense; import org.apache.tapestry5.BindingConstants; import org.apache.tapestry5.Block; import org.apache.tapestry5.ComponentResources; import org.apache.tapestry5.annotations.Import; import org.apache.tapestry5.annotations.Parameter; import org.apache.tapestry5.annotations.Property; import org.apache.tapestry5.ioc.annotations.Inject; import org.joda.time.DateTime; @Import(stack = &quot;blogstack&quot;, module = &quot;app/analytics&quot;) public class Layout { @Parameter(defaultPrefix = BindingConstants.LITERAL) @Property(read = false) private String title; @Parameter(defaultPrefix = BindingConstants.LITERAL) @Property(read = false) private String subtitle; @Parameter(defaultPrefix = BindingConstants.BLOCK) @Property private Block aside1; @Parameter(defaultPrefix = BindingConstants.BLOCK) @Property private Block aside2; @Parameter(defaultPrefix = BindingConstants.BLOCK) @Property private Block aside3; @Parameter(defaultPrefix = BindingConstants.BLOCK) @Property private Block aside4; @Parameter @Property private Adsense adsense; @Property private String page; @Inject ComponentResources resources; void setupRender() { page = resources.getPageName(); } public int getYear() { return DateTime.now().getYear(); } public String getTitle() { if (title == null) { return String.format(&quot;%s&quot;, getSubtitle()); } else { return String.format(&quot;%s | %s&quot;, title, getSubtitle()); } } public String getSubtitle() { return (subtitle == null) ? &quot;Blog Stack&quot; : subtitle; } public String getContentClass() { return (isAside()) ? &quot;col-xs-12 col-sm-12 col-md-8 content&quot; : &quot;col-xs-12 col-sm-12 col-md-12 content&quot;; } public boolean isAside() { return (aside1 != null || aside2 != null || aside3 != null || aside4 != null); } }</code></pre></noscript>

El archivo tml asociado al componente plantilla será el que genere el contenido html que se enviará al navegador del usuario. En esta plantilla se incluye una cabecera con el logo de la aplicación y una frase que lo describe, posteriormente está una barra de navegación con varios enlaces, con <t:body/> se incluye el contenido propio de la página que usa el componente plantilla y usando el componente <t:delegate/> se incluye el contenido de los diferentes bloques aside si se han personalizado en el uso de la plantilla, con el componente <t:if test=“aside”> se comprueba si hay algún aside usándose el método isAside de la clase Layout asociada al componente plantilla y del tml. Finalmente, está el pie que será común a todas las páginas que usen este componente.

<noscript><pre><code>&lt;!DOCTYPE html&gt; &lt;html xmlns=&quot;http://www.w3.org/1999/xhtml&quot; xmlns:t=&quot;http://tapestry.apache.org/schema/tapestry_5_4.xsd&quot; xmlns:p=&quot;tapestry:parameter&quot;&gt; &lt;head&gt; &lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=utf-8&quot;/&gt; &lt;meta t:type=&quot;any&quot; t:pagina=&quot;${page}&quot; /&gt; &lt;title&gt;${title}&lt;/title&gt; &lt;!-- Resources --&gt; &lt;link href=&quot;//fonts.googleapis.com/css?family=Open+Sans:400,700&quot; rel=&quot;stylesheet&quot; type=&quot;text/css&quot;/&gt; &lt;link href=&quot;//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css&quot; rel=&quot;stylesheet&quot; type=&quot;text/css&quot;/&gt; &lt;link href=&quot;/feed.atom.xml&quot; rel=&quot;alternate&quot; type=&quot;application/atom+xml&quot; title=&quot;Portada&quot;/&gt; &lt;link href=&quot;${context:images/favicon.png}&quot; rel=&quot;icon&quot; type=&quot;image/png&quot;/&gt; &lt;/head&gt; &lt;body&gt; &lt;header&gt; &lt;div class=&quot;container-fluid&quot;&gt; &lt;div class=&quot;row&quot;&gt; &lt;div class=&quot;col-xs-12 col-sm-12 col-md-4&quot;&gt; &lt;h1&gt;&lt;a t:type=&quot;pagelink&quot; page=&quot;index&quot; class=&quot;blogstack&quot;&gt;&lt;span class=&quot;glyphicon glyphicon-th&quot;&gt;&lt;/span&gt; Blog &lt;span class=&quot;stack&quot;&gt;Stack&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt; &lt;/div&gt; &lt;div id=&quot;horizontalSkycraper&quot; class=&quot;col-xs-12 col-sm-12 col-md-8&quot;&gt;&lt;/div&gt; &lt;/div&gt; &lt;div class=&quot;row&quot;&gt; &lt;div class=&quot;col-xs-12 col-sm-12 col-md-12&quot;&gt; &lt;h4&gt;Un poco más que un agregador/planeta de bitácoras sobre programación, desarrollo, software libre, gnu/linux, tecnología, ...&lt;/h4&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;/header&gt; &lt;div class=&quot;container-fluid&quot;&gt; &lt;div class=&quot;row&quot;&gt; &lt;div class=&quot;col-xs-12 col-sm-12 col-md-12&quot;&gt; &lt;nav role=&quot;navigation&quot;&gt; &lt;ul class=&quot;nav nav-pills menu&quot;&gt; &lt;li&gt;&lt;a t:type=&quot;pagelink&quot; page=&quot;index&quot;&gt;Inicio&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a t:type=&quot;pagelink&quot; page=&quot;archive&quot; context=&quot;[]&quot;&gt;Archivo&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a t:type=&quot;pagelink&quot; page=&quot;faq&quot;&gt;Preguntas frecuentes&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt; &lt;/nav&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;div class=&quot;container-fluid&quot;&gt; &lt;div class=&quot;row&quot;&gt; &lt;div t:type=&quot;any&quot; class=&quot;prop:contentClass&quot;&gt;&lt;t:body /&gt;&lt;/div&gt; &lt;t:if test=&quot;aside&quot;&gt; &lt;aside class=&quot;col-xs-12 col-sm-12 col-md-4&quot;&gt; &lt;t:socialnetworks/&gt; &lt;t:if test=&quot;aside1&quot;&gt; &lt;t:delegate to=&quot;aside1&quot;/&gt; &lt;/t:if&gt; &lt;div id=&quot;bigRectangle&quot;&gt;&lt;/div&gt; &lt;t:if test=&quot;aside2&quot;&gt; &lt;t:delegate to=&quot;aside2&quot;/&gt; &lt;/t:if&gt; &lt;div class=&quot;row&quot;&gt; &lt;div class=&quot;col-xs-3 col-md-2&quot;&gt; &lt;div id=&quot;wideSkycraper&quot;&gt;&lt;/div&gt; &lt;/div&gt; &lt;t:if test=&quot;aside3&quot;&gt; &lt;div class=&quot;col-xs-3 col-md-2&quot;&gt; &lt;t:delegate to=&quot;aside3&quot;/&gt; &lt;/div&gt; &lt;/t:if&gt; &lt;/div&gt; &lt;t:if test=&quot;aside4&quot;&gt; &lt;t:delegate to=&quot;aside4&quot;/&gt; &lt;/t:if&gt; &lt;/aside&gt; &lt;/t:if&gt; &lt;/div&gt; &lt;/div&gt; &lt;footer&gt; &lt;div class=&quot;container-fluid&quot;&gt; &lt;div class=&quot;row&quot;&gt; &lt;div class=&quot;col-xs-12 col-sm-12 col-md-12&quot;&gt; &lt;div class=&quot;footer&quot;&gt; &lt;a t:type=&quot;pagelink&quot; page=&quot;index&quot;&gt;Blog Stack&lt;/a&gt; por &lt;a href=&quot;https://twitter.com/picodotdev/&quot;&gt;pico.dev&lt;/a&gt; está publicado bajo la licencia de software libre &lt;a href=&quot;http://www.gnu.org/licenses/agpl-3.0.html&quot;&gt;GNU Affero General Public&lt;/a&gt;.&lt;br/&gt; El contenido agregado conserva la licencia de su bitácora.&lt;br/&gt; «Powered by» &lt;a href=&quot;https://github.com/picodotdev/blogstack&quot;&gt;Blog Stack&lt;/a&gt;, &lt;a href=&quot;http://tapestry.apache.org/&quot;&gt;Apache Tapestry&lt;/a&gt;, &lt;a href=&quot;https://www.openshift.com/&quot;&gt;OpenShift&lt;/a&gt;, &lt;a href=&quot;https://pages.github.com/&quot;&gt;GitHub Pages&lt;/a&gt;, &lt;a href=&quot;http://www.oracle.com/es/technologies/java/overview/index.html&quot;&gt;Java&lt;/a&gt; y más software libre o de código abierto, inspirado en &lt;a href=&quot;http://octopress.org/&quot;&gt;Octopress&lt;/a&gt;.&lt;br/&gt; &lt;span class=&quot;copyleft&quot;&gt;&amp;copy;&lt;/span&gt; pico.dev ${year} &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;/footer&gt; &lt;div id=&quot;fb-root&quot;&gt;&lt;/div&gt; &lt;t:ads adsense=&quot;adsense&quot;/&gt; &lt;/body&gt; &lt;/html&gt;</code></pre></noscript>

Para terminar nos queda ver como sería usar este componente en una página donde queremos usarlo. En la etiqueta html se usa la plantilla con t:type para indicar que esa etiqueta es un componente de Tapestry y se le pasan los aside1 y aside2 que en esta página tienen contenido propio. El contenido de la etiqueta html se sustituirá por la etiqueta <t:body/> de la plantilla, el contenido inlcluido en los componentes <t:block/> aunque esté dentro de la etiqueta html solo se mostrará cuando se haga uso de un <t:delegate/>, como se hace el componente plantilla. Este es el caso de la página índice de Blog Stack. A pesar de todo el contenido que genera y solo consta de 34 líneas de código, esto muestra lo fácil que es en Tapestry dividir las diferentes partes de una página en componentes que puede ser reutilizados.

<noscript><pre><code>&lt;html t:type=&quot;layout&quot; t:aside1=&quot;aside1&quot; t:aside2=&quot;aside2&quot; xmlns:t=&quot;http://tapestry.apache.org/schema/tapestry_5_4.xsd&quot; xmlns:p=&quot;tapestry:parameter&quot;&gt; &lt;t:data/&gt; &lt;t:loop source=&quot;posts&quot; value=&quot;post&quot;&gt; &lt;t:postcomponent post=&quot;post&quot; excerpt=&quot;true&quot;/&gt; &lt;/t:loop&gt; &lt;section class=&quot;index-pagination&quot;&gt; &lt;div class=&quot;container-fluid&quot;&gt; &lt;div class=&quot;row&quot;&gt; &lt;div class=&quot;col-xs-4 col-sm-4 col-md-4&quot;&gt; &lt;t:if test=&quot;!lastPage&quot;&gt; &lt;a t:type=&quot;pagelink&quot; page=&quot;index&quot; context=&quot;nextContext&quot;&gt;&lt;span class=&quot;glyphicon glyphicon-arrow-left&quot;&gt;&lt;/span&gt; Más antiguo&lt;/a&gt; &lt;/t:if&gt; &lt;/div&gt; &lt;div class=&quot;col-xs-4 col-sm-4 col-md-4 col-xs-offset-4 col-sm-offset-4 col-md-offset-4 text-right&quot;&gt; &lt;t:if test=&quot;!firstPage&quot;&gt; &lt;a t:type=&quot;pagelink&quot; page=&quot;index&quot; context=&quot;previusContext&quot;&gt;Más nuevo &lt;span class=&quot;glyphicon glyphicon-arrow-right&quot;&gt;&lt;/span&gt;&lt;/a&gt; &lt;/t:if&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;/section&gt; &lt;t:block id=&quot;aside1&quot;&gt; &lt;t:feeds/&gt; &lt;/t:block&gt; &lt;t:block id=&quot;aside2&quot;&gt; &lt;t:lastposts/&gt; &lt;t:lastsourceswithposts/&gt; &lt;/t:block&gt; &lt;/html&gt;</code></pre></noscript>

Usando el mismo componente podemos darle un aspecto común pero variando el contenido de las diferentes secciones. En este caso usamos la misma plantilla donde se muestra la misma cabecera, enlaces de navegación y pie de página pero sin el contenido lateral como en el caso de la página de preguntas frecuentes de Blog Stack, en este caso no usamos los componentes aside.

<noscript><pre><code>&lt;html t:type=&quot;layout&quot; t:title=&quot;Preguntas frecuentes&quot; xmlns:t=&quot;http://tapestry.apache.org/schema/tapestry_5_4.xsd&quot; xmlns:p=&quot;tapestry:parameter&quot;&gt; &lt;article class=&quot;text-justify&quot;&gt; &lt;header&gt; &lt;h1&gt;Preguntas frecuentes&lt;/h1&gt; &lt;/header&gt; &lt;h2&gt;¿Qué es Blog Stack?&lt;/h2&gt; &lt;p&gt;Blog Stack (BS) es una agregador, planeta, o fuente de información de bitácoras sobre programación, desarrollo, desarrollo ágil, software, software libre, hardware, gnu/linux o en general temas relacionados con la tecnología.&lt;/p&gt; &lt;h2&gt;¿Por qué otro agregador?&lt;/h2&gt; &lt;p&gt; Hay varios motivos, la semilla es que quería hacer un proyecto personal con cierta utilidad para otras personas empleando de alguna forma el framework para el desarrollo de aplicaciones web &lt;a href=&quot;http://tapestry.apache.org/&quot;&gt;Apache Tapestry&lt;/a&gt;. &lt;/p&gt; ... &lt;/article&gt; &lt;/html&gt; </code></pre></noscript>

Por supuesto, podemos crear tantos componentes plantilla como necesitemos en una aplicación y usar uno o otro en función del tipo de página.

Referencia:
Código fuente de Blog Stack
Libro PlugIn Tapestry
Documentación sobre Apache Tapestry

Author: "--"
Send by mail Print  Save  Delicious 
Next page
» You can also retrieve older items : Read
» © All content and copyrights belong to their respective authors.«
» © FeedShow - Online RSS Feeds Reader