Thursday, August 31, 2006

Arreglos de metodos en C#

Cuantas veces has escrito codigo mas o menos asi?:

if (someValue == SomeEnum.Type1)
Method1("value 1");
else if (someValue == SomeEnum.Type2)
Method2("value 2");
else if (someValue == SomeEnum.Type2)
Method3("value 3");

O talvez usaste un "switch" para lograr esto mismo.
Inmediatamente podemos ver un patron ahi: Metodo1, Metodo2 y Metodo3 todos tienen la misma estructura y se ejecutan cuando pasamos los valores Type1, Type2 y Type3. No seria bueno si pudieramos reducir ese codigo a una sola linea?


Eso es exactamente lo que los arreglos de metodos pueden hacer por ti, vamos a ver un ejemplo, antes de poderlos usar tenemos que prepararlos.

Como todos los metodos comparten la misma estructura, esto quiere decir que podemos usar un delegado para representar todos los metodos

delegate void AddStringDelegate(string someValue);

Ahora declaramos un arreglo de delegados:

AddStringDelegate[] addStringMethods;

tambien vamos a necesitar un tipo enum para accesar los metodos del arreglo:
enum StringType {
Type1,
Type2,
Type3,
Type4
}

Luego tenemos los metodos en si:

void AddStringType1(string someValue) {
OutputText(string.Format("String Type 1: {0}", someValue));
}
void AddStringType2(string someValue) {
OutputText(string.Format("String Type 2: {0}", someValue));
}
void AddStringType3(string someValue) {
OutputText(string.Format("String Type 3: {0}", someValue));
}
Finalmente, en el constructor de nuestra clase, podemos asignar los metodos al arreglo usando el delegado y de esta manera obtenemos nuestro arreglo de metodos:
addStringMethods = new AddStringDelegate[3];
addStringMethods[(int)StringType.Type1] = new AddStringDelegate(AddStringType1);
addStringMethods[(int)StringType.Type2] = new AddStringDelegate(AddStringType2);
addStringMethods[(int)StringType.Type3] = new AddStringDelegate(AddStringType3);

Ya esta listo para ser usado, vamos a ver como se veria el codigo que presente al inicio, ahora usando nuestro arreglo:

public void AddString(string someValue, StringType stringType) {
addStringMethods[(int)stringType](someValue);
}

Como puedes ver, podemos accesar el metodo requerido pasando el tipo (y convirtiendolo a entero porque C# no nos deja hacerlo si no lo convertimos); nuestro codigo ahora se redujo a una simple linea, pasamos el enum stringType y este ejecuta el metodo correcto automaticamente.


El uso de esta tecnica por ahora se las dejo de tarea a ustedes, yo no recomendaria usar esta tecnica en una clase pequeña que creamos y tiramos constantemente, pero en algunos casos talvez aun asi podria ser util.


Yo creo que seria especialmente util si tuvieramos este tipo de patron (donde queremos ejecutar ciertos metodos dependiendo de ciertos values de enumeracion) dentro de un patron singleton, o donde la clase potencialmente podria vivir mucho mas, podrias hacer todo el trabajo de inicializar tu arreglo de metodos y asi poder reutilizarlos efectivamente.


Aqui esta todo el codigo para este ejemplo, en mi siguiente post les mostrare como podemos usar esta tecnica para implementar un tipo de factory pattern (patron de fabrica) o mas concretamente el simple factory pattern (patron de fabrica simple).


Precisamente hoy mismo acabo de usar este patron (que yo le llamo "simplified simple factory pattern") en mi proyecto actual, donde creo una instancia de la clase generadora y la reuso cada que necesito un objeto nuevo (dependiendo del tipo requerido)

class FileGeneratorBase {
delegate void AddStringDelegate(string someValue);

AddStringDelegate[] addStringMethods;

public FileGeneratorBase() {
addStringMethods = new AddStringDelegate[4];
addStringMethods[(int)StringType.Type1] = new AddStringDelegate(AddStringType1);
addStringMethods[(int)StringType.Type2] = new AddStringDelegate(AddStringType2);
addStringMethods[(int)StringType.Type3] = new AddStringDelegate(AddStringType3);
addStringMethods[(int)StringType.Type4] = new AddStringDelegate(AddStringType4);
}

public void AddString(string someValue, StringType stringType) {
addStringMethods[(int)stringType](someValue);
}
void AddStringType1(string someValue) {
OutputText(string.Format("String Type 1: {0}", someValue));
}
void AddStringType2(string someValue) {
OutputText(string.Format("String Type 2: {0}", someValue));
}
void AddStringType3(string someValue) {
OutputText(string.Format("String Type 3: {0}", someValue));
}
void AddStringType4(string someValue) {
OutputText(string.Format("String Type 4: {0}", someValue));
}
void OutputText(string someValue) {
Console.WriteLine(someValue);
}
}

class Program {
static void Main(string[] args) {
FileGeneratorBase fg = new FileGeneratorBase();
fg.AddString("some value", StringType.Type1);
fg.AddString("some other value", StringType.Type2);
fg.AddString("one last value", StringType.Type3);
fg.AddString("testing out of bounds", StringType.Type4);
Console.ReadLine();
}
}

Actualizado: Para incluir el tipo enum en la inicializacion de los metodos,
en lugar de 0..4 que tenia inicialmente, esto nos permitiria cambiar el orden
de los elementos de la enumeracion

Tuesday, August 29, 2006

Escondiendo la complejidad de los tipos genericos segunda parte

En articulos anteriores hable sobre tipos genericos complejos

tipos genericos multidimensionales en C#

y como hacer esos tipos genericos mas legibles usando genericos con alias

Escondiendo la complejidad de los tipos genericos

using System.Collections.Generic;
using FilesList = List<string>;
using FoldersWithFiles = Dictionary<string, List<string>>;
using ComputersWithFiles = Dictionary<string, Dictionary<string, List<string>>>;

Aqui les presento otra tecnica para lograr codigo legible mientras que usamos tipos genericos complejos.


Podemos crear subclases de tipos genericos de la siguiente manera:

class FilesList2 : List<string> { }
class FoldersWithFiles2 : Dictionary<string, List<string>> { };
class ComputerFiles2 : Dictionary<string, Dictionary<string, List<string>>> { }
class ComputersWithFiles2 : Dictionary<string, Dictionary<string, List<string>>> { }

El codigo para usarlos es practicamente el mismo que cuando usamos genericos "aliasados", pero hay un pequeño "problema", KeyValuePair es un struct, y como tal no soporta herencia, entonces no podemos usar esta tecnica con este tipo, lo que podemos hacer es combinar tipos aliasados para el tipo KeyValuePair y subclases para el resto.


Esto nos lleva a otra cosa, cuando tenemos codigo como este:

Console.WriteLine("Lista de directorios con archivos");
FoldersWithFiles2 foldersWithFiles = GetFoldersWithFiles();
foreach (FoldersWithFilesPair kv in foldersWithFiles) {
Console.WriteLine(string.Format(" Folder: [{0}]", kv.Key));
ListFiles((FilesList2)kv.Value); //***type casting required here!!
}

Si queremos usar FoldersWithFilesPair.Value como un tipo FilesList2 tenemos que hacerle una conversion de tipo, esto es porque FilesList2 es un tipo nuevo y FoldersWithFilesPair.Value es List<string>; recuerda que C# es un lenguaje type-safe (como se dice en español!!!??)


Crear sub clases de tipos genericos de esta manera nos puede dar mas beneficios ademas del codigo legible, como cualquier otra clase puede implementar sus propios metodos y podrias asignarle tareas (o metodos) que reflejen mas correctamente su responsabilidad.


Hay una gran diferencia entre usar tipos aliasados y subclases de genericos, los tipos aliasados son exactamente el mismo tipo, solo con un nombre diferente, las subclases de genericos tienen un nombre diferente y son en si un tipo nuevo.


Eso es todo por ahora, ahi te dejo esta herramienta para tu arsenal, como todo lo demas en esta vida, no hay que abusar de esta tecnica!


Espero ver algun codigo que implemente estas tecnicas


Como les habia prometido, aqui esta el codigo fuente, el codigo es gratis tal cual como en la licencia LGPL


salu2

Sunday, August 27, 2006

La iglesia de... Google?

Pues si, ya ven que para todo hay gente (si hay una religion donde Maradona es Dios, porque no habria una de Google)

Por ahora el sitio esta solo en ingles, quiensabe si luego se avienten con otros idiomas

Friday, August 25, 2006

finally {} no es un buen lugar para confirmar las transacciones

Acabo de arreglar un codigo que estaba mas o menos asi::

transaction = connection.BeginTransaction();
try {
try
{
//***.... algunas operaciones en la DB aqui...
} finally {
transaction.Commit();
}
}
catch {
transaction.RollBack();
}

Primero que nada, hay un bug ahi, si el codigo de enmedio nos diera una exception, este codigo generaria una nueva exception en la seccion del catch{} porque no podemos hacer rollback a una transaccion que ya se le hizo commit (el mensaje no sera asi de claro, pero eso es lo que quiere decir, la excepcion dice algo como que la operacion no puede ser completada debido al estado actual del objeto), y perderas la informacion de la excepcion original


Segundo, simplemente esta mal!, quita cualquier beneficio que tenias con la transaccion, es como decir:


//*** borrar algunos registros


//*** actualizar algunos registros


//*** alguna operacion que causo una excepcion


//*** finalmente, confirmar lo que halla hecho hasta ahorita


Dejando tus datos en un estado indeterminado (no sabes hasta donde llego tu codigo antes del error) porque confirmaste la operacion hasta donde los errores hallan ocurrido.


pista: siempre usa "using" para cualquier cosa relacionada con la base de datos (connecciones, commandos, transacciones, etc)

update: gracias a Kamikaze por encontrar un bug

Thursday, August 24, 2006

Escondiendo la complejidad de los tipos genericos

En mi ultimpo post les mostre un uso un tanto raro de los tipos genericos, Ayende dice que no puede leer ese codigo y nos da una alternativa; yo estoy de acuerdo, el codigo es feo, pero aun hay esperanza!

Aqui esta el codigo equivalente a la primera seccion (aun estoy usando tipos genericos, aunque parezca que no es asi), donde queriamos guardar una lista de nombres de archivos:

ListaArchivos listaArchivos = CargarListaArchivos();
ListarArchivos(listaArchivos);

Luego tenemos los genericos bidimensionales, para guardar el nombre del folder y la lista de archivos:

FoldersConArchivos foldersConArchivos = CargarFoldersConArchivos();
foreach (ParFoldersConArchivos kv in foldersConArchivos) {
Console.WriteLine(string.Format(" Folder: [{0}]", kv.Key));
ListarArchivos(kv.Value);
}

Finalmente donde guardamos el nombre de la computadora, con los folders, con los archivos que contiene:

ArchivosEnComputadoras archivosEnComputadoras = CargarArchivosEnComputadoras();
foreach (ParComputadorasConFolders kv in archivosEnComputadoras) {
Console.WriteLine(string.Format("Computer: [{0}]", kv.Key));
foreach (ParFoldersConArchivos kv1 in kv.Value) {
Console.WriteLine(string.Format(" Folder: [{0}]", kv1.Key));
ListarArchivos(kv1.Value);
}
}

Y mi ultimo ejemplo ahora se veria asi:

ArchivosEnComputadoras archivosEnComputadoras = new ArchivosEnComputadoras();

En vez de esto:

Dictionary<string, Dictionary<string, List<string>>> archivosEnComputadoras = 
new Dictionary<string, Dictionary<string, List<string>>>();

Ahora, si el codigo es equivalente y aun estoy usando tipos genericos, que hice?


Estoy usando genericos con alias (sip, tambien me invente este termino J) los cuales me dan nombres mucho mas amigables que los genericos, en vez de la sintaxis de los genericos multidimensionales.


Aqui estan las declaraciones de los genericos con alias:

using System.Collections.Generic;
using ParFoldersConArchivos = KeyValuePair<string, List<string>>;
using ParComputadorasConFolders = KeyValuePair<string, Dictionary<string, List<string>>>;

using ListaArchivos = List<string>;
using FoldersConArchivos = Dictionary<string, List<string>>;
using ArchivosEnComputadoras = Dictionary<string, Dictionary<string, List<string>>>;

Esto nos da lo mejor de dos mundos, puedes tener tipos complejos de genericos y a la vez mantener tu codigo limpio.


Pones todo lo feo en un lugar nadamas y con eso ya puedes usar nombres mas amigables que se ven como nombres de clases normales, ademas esto tambien hace tu codigo muy extensible, ya que potencialmente podrias cambiar la definicion de los tipos (y aun reemplazarlos por clases) sin quebrar el codigo.


En mi siguiente post les mostrare otra tecnica para lograr el mismo efecto, y algunas ventajas/desventajas de dicho metodo, luego subire todo el codigo fuente en un archivo .zip para que lo puedan bajar todo junto.


salu2

Tuesday, August 22, 2006

tipos genericos multidimensionales en C#

Uno puede explicar los tipos Genericos como arreglos en asteroides, o collecciones en asteroides o una combinacion de los dos, los tipos genericos nos ofrecen una lista expandible de elementos type-safe (tipos de datos seguros?); o como lo describe MSDN:

Genericos te permiten definir estructuras de datos type-safe, sin que estas sean especificas a tipos de datos actuales.

Una cosa que no he visto mucho es como se pueden usar genericos multidimensionales (asi es, yo me invente el termino J), permitanme explicarles a que me refiero.

Vamos a decir que queremos guardar una lista de nombres de archivo, podriamos usar algo asi::



List<string> listaDeArchivos = CargarListaArchivos();
ListarArchivos(listaDeArchivos);

Ahora necesitamos una lista de directorios con los archivos que contienen, entonces podemos usar un tipo generico bidimensional que contenga el nombre del directorio en la llave de un diccionario y la lista de archivos en el valor del diccionario:

Dictionary<string, List<string>> directoriosConArchivos =
CargarDirectoriosConArchivos();
foreach (KeyValuePair<string, List<string>> kv in directoriosConArchivos) {
Console.WriteLine(string.Format(" Folder: [{0}]", kv.Key));
ListarArchivos(kv.Value);
}

finalmente, digamos que queremos una lista de computadoras, con los directorios y los archivos que estos contienen; para esto podemos usar un tipo generico multidimensional que tenga el nombre de la computadora en la llave de un diccionario y los folders con archivos en el valor de este diccionario (ya se complica el asunto)

Dictionary<string, Dictionary<string, List<string>>> archivosEnComputadoras =
CargarListaDeArchivosEnComputadoras();
foreach (KeyValuePair<string, Dictionary<string, List<string>>> kv in archivosEnComputadoras ) {
Console.WriteLine(string.Format("Computadora: [{0}]", kv.Key));
foreach (KeyValuePair<string, List<string>> kv1 in kv.Value) {
Console.WriteLine(string.Format(" Folder: [{0}]", kv1.Key));
ListarArchivos(kv1.Value);
}
}

Solo como algo adicional, aqui les dejo la funcion ListarArchivos, que muestra un uso interesante de los genericos combinado con delegados anonimos

static void ListFiles(List<string> filesList) {
filesList.ForEach(new Action<string>(delegate(string s) {
Console.WriteLine(string.Format(" {0}", s)); }));
}

Como pueden ver, con los tipos genericos se pueden crear estructuras de datos muy complejas que mantienen el type-safe (como se dice esto!!!), peeeeeero... como siempre, siempre habra quien se queje de que los tipos genericos multidimensionales se tornan muy cripticos (yo soy el primero en quejarme);
Esto se puede ilustrar simplemente escribiendo algo asi:


Dictionary<string, Dictionary<string, List<string>>> archivosEnComputadoras =
new Dictionary<string, Dictionary<string, List<string>>>();

En mi siguiente post, les mostrare un par de tecnicas de como podemos esconder toda esta complejidad y aun asi mantener los beneficios de los tipos genericos

salu2

Blogueando desde 1969

 Alguna gente simplemente han estado blogueando por mucho tiempo...

(ah si, tambien queria probar la capacidad de LiveWriter para subir imagenes)

Tuesday, August 15, 2006

RegisterChannel is obsolete, use RegisterChannel instead

me tope con este error:

Warning 6 'System.Runtime.Remoting.Channels.ChannelServices.RegisterChannel(System.Runtime.Remoting.Channels.IChannel)' is obsolete: 'Use System.Runtime.Remoting.ChannelServices.RegisterChannel(IChannel chnl, bool ensureSecurity) instead.'

me saco de onda un poco porque System.Runtime.Remoting.ChannelServices.RegisterChannel no existe realmente

El problema es simplemente que les falto .Channels  en la parte del namespace, asi que debio ser:

"use 'System.Runtime.Remoting.Channels.ChannelServices.RegisterChannel(IChannel chnl, bool ensureSecurity) instead"

ok, esa fue la primera parte, asi que este overload ahora es obsoleto, pero cual es el equivalente?

RegisterChannel(chnl, true)

o

RegisterChannel(chnl, false)

Reflector al rescate, el equivalente es usar false como el segundo parametro, es justo lo que hace la funcion actual, cuando llamams

RegisterChannel(chnl);

este a su vez llama:

ChannelServices.RegisterChannelInternal(chnl, false);

Monday, August 14, 2006

Primer bug en live write

Esto pasa cuando tienes multiples cuentas de blogs, en el ultimo dialogo, si no checas la opcion "switch to this blog now" (o algo asi); el nuevo blog no aparecera en tu lista de blogs, y no te podras cambiar a este

reiniciando la aplicacion soluciona el problema

Hola mundo usando live writer

Esta buenisimo esto, me permite escribir a multiples blogs sin ningun problema, pueden checar el blog de live writer aqui

Definitivamente mi nueva herramienta para escribir blogs

uso peculiar del operador coallesce ?? en C#

Acabamos de escribir un codigo que contiene algo asi (ignoren los nombres, solo estoy ilustrando el punto):

return misCollecciones.Servicios ?? (misCollecciones.Servicios = db.CargarServicios());

one sola linea que hace varias cosas, y aun asi es claro lo que se esta haciendo; si ya conoces el operador ?? talvez ya hasta aqui sea suficiente, si no lo sabes, he aqui una explicacion un poco mas extendida

el operador ?? regresa el operador de la izquierda, si es que este no es NULL, de lo contrario regresa el operador de la derecha

osea, que este codigo es equivalente a:

if (misCollecciones.Servicios == null)

misCollecciones.Servicios = db.CargarServicios();

return misCollecciones.Servicios;

Wednesday, August 09, 2006

Curiosidades: Como usar Notepad para crear un log

1. Corre Notepad
2. Escribe .LOG en la primera linea
3. Graba el archivo donde prefieras. La proxima vez que abras el archivo, tendra la fecha y hora actual y puedes escribir un texto ahi, y asi sucesivamente cada vez que abras el archivo (en Notepad) se agregara la fecha y hora, para que tu escribas un texto ahi enseguida

muy util para cuando uno esta haciendo actualizaciones o cosas asi que quieres mantener un rastro de lo que se hizo y a que hora

Hasta eso que este feature si esta documentado en Microsoft!

How to Use Notepad to Create a Log File

salu2

Delphi por fin sera gratis

mas o menos, parece que esta onda de Borland que esta vendiendo sus productos de desarrollo por ahi va ya, quierendo tomar forma; acaban de anunciar estas versiones "express" (Gratis) y versiones rasonablemente mucho mas baratas de Delphi, C# y C++

Turbo Delphi, Turbo Delphi for .NET, Turbo C++ y Turbo C# estaran disponibles al publico en el tercer cuarto del 2006. Las versiones "Turbo Explorer" de estos productos seran gratis.  El Precio para las versiones "Turbo Profesional" sera menos de 500 dlls. Precios para estudiantes para las versiones "Turbo Profesional" sera menos de 100 dlls. Para mas informacion visite www.turboexplorer.com.

August 8, 2006 - Turbos Press Release

Ya veremos que pueden hacer contra las versiones Express de Microsoft que ya tienen buen tiempo y que tambien son gratis y de muy buena calidad, ahora mismo no veo como es que pudieran competir con MS, pero ya se vera; si estas versiones corren (y probablemente asi sera) contra el framework 1.1 mucha gente no estara interesada en siquiera bajarlas

salu2

Sunday, August 06, 2006

El problema de nombrar tu blog como alguna tecnologia

Especialmente en Microsoft, los nombres de las tecnologias cambian, y algunas veces cambian frecuentemente, he visto algunos blogs nombrados como cierta tecnologia de MS que luego ha cambiado el nombre, entre los mas recientes encontramos Vista y PowerShell

Saturday, August 05, 2006

No puedes saberlo todo, entonces que hacer?

No puedes saberlo todo. No importa que tan inteligente seas, no importa la educacion que hallas tenido, no importa toda la experiencia que tengas, simplemente no hay forma de adquirir todo el conocimiento que necesitas para hacer que tu negocio crezca.
Donald Trump - Learning - Business - Knowledge

Learning Quotes

En el mundo tan amplio de la programacion, oimos esto cada vez mas seguido, y es que los frameworks crecen a niveles masivos, los frameworks actuales tienen miles de clases, funciones, etc. Aun la gente detras de esos mismos frameworks no sabe otras partes del mismo, que te hace pensar que tu lo puedes aprender todo?
Es clave aceptar la ignorancia propia. Entender que no puedes saber todo y dejar de pretender que puedes siquiera hacerlo. Entender que otra gente tambien es ignorante, y ayudar a educarlos antes que sentirte superior a ellos. Asi tambien, no rechaces a alguien que esta tratando de enseñarte algo, tragate tu orgullo y se mejor con esto mismo.

The Critical Path / eMail Newsletter / Issue 6.1

La forma que yo ataco este problema es aprender las caracteristicas del lenguaje en si y todo lo que es posible hacer con este a un nivel tan alto como pueda; luego en las caracteristicas de los frameworks hago una distincion entre las tecnologias que quiero aprender en este momento y las aprendo, y las demas tecnologias que me pudieran ser de utilidad mas adelante, pero la cosa es saber que existen y que es lo que hacen, y asi cuando las necesitas las puedes encontrar mucho mas rapido porque sabes que hay algo para hacer justamente eso y podras evitar reinventar la rueda en muchos casos.

Servicios como del.icio.us tambien nos pueden ayudar con esto, si encuentras algo que crees que podria ser util mas adelante puedes guardarlo en tus "favoritos" y ponerle algo como "futuro" y asi mismo puedes construir una lista de cosas que te seran de utilidad mas adelante

No puedes poner todo en tu cerebro, pero puedes usar las herramientas actuales para hacer algo de ese trabajo por ti.

Es justamente como en el futbol, siempre es bueno ganar, pero si no puedes ganar es bueno empatar, y si no puedes empatar es bueno perder por la menos diferencia de goles posible, asi mismo es el conocimiento

seria bueno saberlo, pero si no podemos saberlo como expertos, seria bueno tener una idea al menos, y si no tenemos idea, seria bueno tener guardados documentos (o links) que te permitan rapidamente encontrar como es que funciona tal o tal cosa

salu2

Mejora la navegacion en tabs

Estas extensiones mejoran muchisimo el comportamiento de los tabs en Firefox y Flock

Ctrl Tab Preview
Este extension reemplaza el comportamiento del Ctrl+Tab. En vez de cambiar el tab directamente, funciona algo asi como el Alt+Tab de windows (con vista previa). Te muestra la vista previa de los tabs y puedes cambiarla ahi o puedes recorrerlas para ver que tienes en cada una de ellas. Una foto dice mas que mil palabras, asi que mira la foto y haber si te animas a bajarla.


SessionSaver
SessionSaver restaura tu navegador exactamente como lo dejaste, siempre que corras tu navegador. Incluso sobrevive a una maquina bloqueada (despues de resetear). Todos los tabs que tenias abiertos, incluso lo que estabas escribiendo, todo se guarda. Usa el menu para agregar o remover sesiones; click izquierdo, derecho o central las borraran. "Modo simple" para estar mas tranquilo o "modo Experto" para flexibilidad mas avanzada.

Perma Tabs
Nos da la posibilidad de hacer que ciertos tabs sean permanentes, osea que no se pueden cerrar, incluso despues de cerrar y volver a abrir el navegador, muy util si queremos tener cierta pagina abierta SIEMPRE.

salu2

Multiples paginas de inicio en tu navegador

Esta es una de esas cosas que mucha gente no sabe, los navegadores modernos de la actualidad nos permiten tener mas de una pagina de inicio, muy util porque generalmente uno visita mas de una pagina todos los dias, talvez la de noticias y alguna otra (al menos), con esto nos permite siempre abrir mas de una pagina al abrir el navegador

Firefox/Flock



Internet Explorer



Opera tambien nos permite hacer esto, es un tanto mas elaborado pero es posible

Multiples paginas de inicio en Opera

En Firefox/Flock tambien existe una extension que permite abrir una pagina de inicio diferente (de las paginas de inicio configuradas) cada vez que se abre el navegador

Home Page Cycler
Carga una pagina de inicio diferente cada vez que inicias Firefox/Flock

Tuesday, August 01, 2006

No dar click (de verdad)

Pues eso... un sitio en el que de verdad, el objetivo es no dar click y aun asi poder navegar, bastante diferente el concepto, como lo ven?

No dar click