El archivo distribuido, un zip, contiene siete chuletas en formato PDF:
- Extensiones a los tipos
String
yObject
- Extensiones a los tipos
Number
yError
- Referencia del tipo
DomEvent
- Extensiones al tipo
DomElement
- Extensiones a los tipos
Date
yBoolean
- Eventos del ciclo de vida en cliente
- Extensiones al tipo
Array
Publicado en: www.variablenotfound.com.
Publicado por José M. Aguilar a las 10:01 p. m.
Etiquetas: ajax, asp.net, chuletas, desarrollo, javascript, programación
C# 3.0 nos trae otra sorpresa, también relacionada con el establecimiento de valores iniciales de elementos: los inicializadores de colecciones. Aunque esta característica también estaba prevista para VB.Net 9.0, al final fue desplazada a futuras versiones por problemas de tiempo.
Para inicializar una colección, hasta ahora era necesario en primer lugar crear la clase correspondiente para, a continuación, realizar sucesivas invocaciones al método
Add()
con cada uno de los elementos a añadir: List<string> ls = new List<string>();
ls.Add("Uno");
ls.Add("Dos");
ls.Add("Tres");
C# 3.0 permite una alternativa mucho más elegante y rápida de codificar, simplemente se introducen los elementos a añadir a la colección entre llaves (como se hacía con los inicializadores de arrays o los nuevos inicializadores de objetos), separados por comas, como en el siguiente ejemplo:
List<string> ls =
new List<string>() { "Uno", "Dos", "Tres" };
Si desensamblamos el ejecutable resultante, podremos ver que es el compilador el que ha añadido por nosotros los
Add()
de cada uno de los elementos después de instanciar la colección:
newobj instance void class
[mscorlib]System.Collections.Generic.List`1<string>::.ctor()
stloc.s '<>g__initLocal0'
ldloc.s '<>g__initLocal0'
ldstr "Uno"
callvirt instance void class
[mscorlib]System.Collections.Generic.List`1<string>::Add(!0)
nop
ldloc.s '<>g__initLocal0'
ldstr "Dos"
callvirt instance void class
[mscorlib]System.Collections.Generic.List`1<string>::Add(!0)
nop
ldloc.s '<>g__initLocal0'
ldstr "Tres"
callvirt instance void class
[mscorlib]System.Collections.Generic.List`1<string>::Add(!0)
nop
Uniendo esto ahora con los inicializadores de objetos que ya tratamos un post anterior, fijaos en la potencia del resultado:
List<Persona> lp = new List<Persona>
{
new Persona { Nombre="Juan", Edad=34 },
new Persona { Nombre="Luis", Edad=53 },
new Persona { Nombre="José", Edad=23 }
};
Efectivamente, cada elemento es una nueva instancia de la clase Persona, con las propiedades que nos interesan inicializadas de forma directa. De hacerlo con los métodos tradicionales, para conseguir el mismo resultado deberíamos utilizar muuuchas más líneas de código.
Otro ejemplo que demuestra aún más la potencia de esta característica:
var nums = new SortedList
{
{ 34, "Treinta y cuatro" },
{ 12, "Doce" },
{ 3, "Tres" }
};
foreach (var e in nums)
Console.WriteLine(e.Key + " " + e.Value);
En el código anterior podemos ver, primero, el uso de variables locales de tipo implícito, para ahorrarnos tener que escribir más de la cuenta. En segundo lugar, se muestra cómo se inicializa una colección cuyo método
Add()
requiere dos parámetros. En el caso de un SortedList<TKey, TValue>
, su método Add()
requiere la clave de ordenación y el valor del elemento.(Obviamente, el resultado de la ejecución del código anterior será la lista ordenada por su valor numérico (Key))
En conclusión, se trata de otra de las innumerables ventajas que nos ofrece la nueva versión de C# destinadas a evitarnos pulsaciones innecesarias, y a la que seguro le daremos uso.
Publicado en: www.variablenotfound.com.
Puedes descargarla pulsando sobre la imagen:
Si quieres leer más sobre estos operadores, puedes probar también en la referencia oficial, The .Net Standard Query Operators [ING], a leer este artículo traducido por el maestro Octavio Hernández, profundizar en MSDN, o en otros de los muchos sitios con información relacionada, como la referencia de Hooked On Linq [ING].
Publicado en: http://www.variablenotfound.com/.
jQuery, para que el no haya oído hablar de ella, es una librería Javascript destinada a facilitar enormemente la vida a los desarrolladores simplificando y unificando el manejo de eventos, la manipulación del contenido (DOM), estilos, el uso de Ajax, la creación de animaciones y efectos gráficos, y un larguísimo etcétera propiciado por la facilidad para añadirle plugins que extienden sus funcionalidades iniciales. Y todo ello de forma muy rápida, sin excesivas complicaciones, y sin añadir demasiado peso a las páginas.
En este post vamos a ver un ejemplo de integración de jQuery con ASP.NET MVC framework realizando una aplicación muy sencilla e ilustrativa que nos enseñará cómo enviar información desde el cliente al servidor y actualizar porciones de página completas con el retorno de éste, respetando en todo momento la filosofía MVC.
El funcionamiento será realmente simple: el usuario introduce su nombre y edad, y al pulsar el botón se enviará esta información al servidor, que la utiliza para componer una respuesta y mandarla de vuelta al cliente. Cuando éste la recibe, la mostrará (con un poco de 'magia' visual de jQuery) y transcurridos unos segundos, desaparecerá de forma automática. La siguiente captura muestra el sistema que vamos a construir en ejecución:
Pero antes de entrar en faena, unos comentarios. En primer lugar, sólo voy a explicar los aspectos de interés para la realización del ejemplo, partiendo de las plantillas adaptadas para Web Developer Express 2008. Si prefieres antes una introducción sobre el framework, puedes visitar las magníficas traducciones de Thinking in .Net de los tutoriales de Scott Guthrië sobre MVC. Se refieren a la primera preview, pero los fundamentos son igualmente válidos.
Segundo, supongo que funcionará con versiones superiores de Visual Studio, pero no he podido comprobarlo. Está creado y comprobado con Visual Web Developer Express, y la Preview 2 del framework MVC.
Y por último, decir que el ejemplo completo podrás descargarlo usando el enlace que encontrarás al final del post. :-)
Primero: Estructuramos la solución
En líneas generales, nuestra aplicación tendrá los siguientes componentes:- Tendremos un controlador principal, llamado Home. En él crearemos dos acciones, las dos únicas que permite nuestra aplicación: una, llamada Index, que se encargará de mostrar la página inicial del sistema, y otra llamada Welcome, que a partir de los datos introducidos por el usuario maquetará el interfaz del mensaje de saludo.
- Como consecuencia del punto anterior, dispondremos de dos vistas. La primera, Index compondrá la interfaz principal con el formulario, y la segunda, que llamaremos Welcome, que define la interfaz del saludo al usuario (el recuadro de color amarillo chillón ;-)).
Esta última vista necesitará los datos de la persona (nombre y edad) para poder mostrar correctamente su mensaje, por lo que el controlador deberá enviárselos después de obtenerlos de los parámetros de la petición.
Fijaos que respetamos en todo momento el patrón MVC haciendo que el cliente invoque a la acción Welcome del controlador usando Ajax, y que la composición del interfaz (HTML) se realice a través de la vista correspondiente. Utilizaremos, por tanto, toda la infraestructura del framework MVC, sin modificaciones. - También, para añadir algo de emoción, he incluido una página maestra, que definirá el interfaz general de las páginas del sistema y realizará la inclusión de los archivos adicionales necesarios, como las hojas de estilo y scripts como jQuery.
Segundo: implementamos el controlador
El controlador de nuestra aplicación va a ser bien simple. Lo vemos y comentamos seguidamente:public class HomeController : Controller
{
public void Index()
{
RenderView("Index");
}
public void Welcome(string name, int age)
{
Person person =
new Person { Age = age, Name = name };
RenderView("Welcome", person);
}
}
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
Podemos observar la clase
HomeController
que implementa las acciones del controlador Home. La acción Index provoca la visualización de la vista de su mismo nombre. La acción Welcome, es algo más compleja; en primer lugar, se observa que recibe dos parámetros, en nombre y edad del usuario, que utiliza para montar un objeto de tipo
Person
, definido algo más abajo, que posteriormente envía a la vista a la hora de renderizarla. Habréis observado aquí la utilización de inicializadores de objetos en la instanciación, y de propiedades automáticas en la definición del tipo.Tercero: implementamos las vistas
Recordemos que vamos a implementar dos vistas, una para la página principal (llamada Index), que mostrará el formulario de entrada de datos, y otra (que llamaremos Welcome) que definirá el interfaz de la respuesta del servidor. Comenzaremos describiendo cómo incluir jQuery en nuestras páginas, y pasaremos después a ellas.3.1. Inclusión de jQuery
Para implementar las vistas necesitamos antes preparar la infraestructura. En primer lugar, descargamos jQuery desde la web oficial del proyecto, y la introducimos en nuestro proyecto. Si vas a descargar la solución completa desde aquí, en ella ya viene incluido el archivo.A continuación, es un buen momento para modificar la página maestra e incluir en ella la referencia a esta librería:
<script type="text/javascript"
src="/scripts/jquery-1.2.3.min.js">
</script>
Un inciso importante aquí. Hace unas semanas se publicó un HotFix para Visual Studio 2008 y Web Developer Express que corrije, entre otras, deficiencias en el intellisense y hacen posible el uso de esta magnífica ayuda cuando escribimos código jQuery. Altamente recomendable, pues, instalarse esta actualización.
Sin embargo, el hecho de introducir en la página maestra la referencia a la librería jQuery hace que el intellisense no funcione como debe. Por tanto, aunque no es la opción que he elegido en este proyecto, podríais incluir el script directamente en la vista Index en lugar de en la Master, y así disfrutaréis del soporte a la codificación.
3.2. La vista "Index"
Esta vista será la encargada de mostrar el formulario e implementar la lógica de cliente necesaria para obtener los datos del usuario, enviarlos al servidor y actualizar el interfaz con el retorno. Su implementación se encuentra en el archivo Index.aspx.Desde el punto de vista del interfaz de usuario es bastante simple, lo justo para mostrar un par de inputs con sus correspondientes etiquetas y el botón que iniciará la fiesta llamando a la función
send()
:<form action="" id="myForm">
<label for="name">Name: </label><input type="text" id="name" />
<br />
<label for="age">Age: </label><input type="text" id="age" size="2" />
<br />
<button id="btn" onclick="send(); return false;">Send!</button>
</form>
<hr />
<div id="result" style="display: none; width: 400px"></div>
Observad que no es necesario establecer un action en el formulario, ni otros de los atributos habituales, pues éste no se enviará (de hecho, el formulario incluso sería innecesario). Fijaos también que el evento
onclick
del botón retorna false, para evitar que se produzca un postback (¡aaargh, palabra maldita! ;-)) del formulario completo.Puede verse también un
div
llamado "result", inicialmente invisible, que se utilizará de contenedor para mostrar las respuestas obtenidas desde el servidor.Pasemos ahora al script. La función
send()
invocada como consecuencia de la pulsación del botón pinta así: function send()
{
var name = document.getElementById("name").value;
var age = document.getElementById("age").value;
updateServerText(name, age);
}
En realidad no hace gran cosa: obtiene el valor de los textboxes y llama a la función que realmente hace el trabajo duro. Este hubiera sido un buen sitio para poner validadores, pero eso os lo dejo de deberes ;-).
El código de la función
updateServerText()
es el siguiente: function updateServerText(name, age)
{
document.getElementById("btn").disabled = true;
$.ajax({
cache: false,
url: '<%= Url.Action("Welcome", "Home") %>',
data: {
Name: name,
Age: age
},
success: function(msg) {
$("#result").html(msg).show("slow");
},
error: function(msg) {
$("#result").html("Bad parameters!").show("slow");
}
});
setTimeout(function () {
document.getElementById("btn").disabled = false;
$("#result").hide("slow");
}, 3000);
}
En primer lugar, se desactiva el botón de envío para evitar nuevas pulsaciones hasta que nos interese. He utilizado un método habitual del DOM,
getElementById()
para conseguirlo, no encontré una alternativa mejor en jQuery.A continuación se utiliza el método
ajax
de jQuery para realizar la llamada al servidor. Aunque existen otras alternativas de más alto nivel y por tanto más fáciles de utilizar, elegí esta para tener más control sobre lo que envío, la forma de hacerlo y la respuesta. Los parámetros utilizados en la llamada a
$.ajax
son:cache
, con el que forzamos la anulación de caché, obligando a que cada llamada se ejecute totalmente, sin utilizar el contenido almacenado en cliente. Internamente, jQuery añade al querystring un parámetro aleatorio, con lo que consigue que cada llamada sea única.url
, la dirección de invocación de la acción, que se genera utilizando el método de ayudaUrl.Action()
, pasándole como parámetros el controlador y la acción, lo que retornará la URL asociada en el sistema de rutas definido. En condiciones normales, si la aplicación se ejecuta sobre el raíz del servidor web, se traducirá por '/Home/Welcome'.data
, los datos a enviar, que se establecen en formato JSON, donde cada propiedad va seguida de su valor. jQuery tomará estos valores y los transformará en los parámetros que necesita la acción Welcome, por lo que el nombre de las propiedades deberá corresponder con los parámetros que espera esta acción (Name y Age).sucess
define la función de retorno exitoso, que mostrará los datos recibidos del servidor introduciéndolos en el contenedor "result". Y ya que estamos, gracias a la magia de jQuery, se mostrará con un efecto visual muy majo.error
, define una función de captura de errores para casos extraños, por si todo falla. Por ejemplo, dado que no estamos validando la entrada del usuario, si éste introduce texto en la edad, el framework no será capaz de realizar la conversión para pasarle los parámetros al controlador y fallará estrepitosamente; en este caso simplemente mostraremos un mensaje de error en cliente.
Fijaos que la llamada a la acción (Welcome) del controlador (Home) es capturada por el framework y dirigida al método correspondiente sin necesidad de hacer nada más, dado que se trata de una llamada HTTP normal. De hecho, si sobre el navegador se introduce la dirección "http://localhost:[tupuerto]/Home/Welcome?Name=Peter&Age=12" podremos ver en pantalla el mismo resultado que recibirá la llamada Ajax.
Obviamente, este efecto podría ser controlado y hacer que sólo se respondieran peticiones originadas a través de Ajax y similares.
Por último, continando con el código anterior, dejamos programado un timer para que unos segundos más tarde, el mensaje mostrado, sea cual sea el resultado de la llamada Ajax, desaparezca lentamente y, de paso, se active de nuevo el botón de envío. El efecto, ya lo veréis si ejecutáis la solución, es de lo más vistoso, creando una sensación de interactividad y dinamismo muy a lo 2.0 que está tan de moda.
3.3. La vista "Welcome"
En esta vista definiremos el interfaz del mensaje que mostraremos al usuario cuando introduzca su información y pulse el botón de envío. Dado que estamos usando MVC, la llamada Ajax descrita anteriormente llegará al controlador y éste hará que la vista cree el interfaz que será devuelto al cliente.La vista, por tanto, es como cualquier otra, salvo algunas diferencias interesantes. Por ejemplo, no tiene página maestra, no la necesita; de hecho ni siquiera tiene la estructura de una página HTML completa, sólo de la porción que necesita para montar su representación. El código de Welcome.aspx, salvo las obligatorias directivas iniciales, es:
<div style="background-color: Yellow; border: 1px solid black;">
<em>Message from server (<%=DateTime.Now %>):</em><br />
Hi, <%= ViewData.Name %>, your age is <%= ViewData.Age %>
</div>
Pero aún queda un detalle que afinar. En el archivo code-behind (o codefile) donde se define la clase
Welcome
hay que indicar expresamente que la clase es una vista de un tipo concreto de la siguiente forma: public partial class Views_WebParts_Welcome : ViewPage<Person>
{
}
De esta forma indicamos que los datos de la vista son del tipo
Person
, lo que nos permite beneficiarnos del tipado fuerte en la composición de la misma; de hecho, esto es lo que permite que podamos usar tan alegremente una expresión como ViewData.Age
a la hora de componer el interfaz.Fijaos que aunque en este ejemplo no hemos hecho ninguna composición compleja y los datos que hemos usado, contenidos en
ViewData
, han sido obtenidos por el Controlador directamente de la vista, sería exactamente igual si se tratara de algo menos simple, como una página concreta de datos obtenidos desde el Modelo, por ejemplo con Linq, y mostrados en forma de grid. Cuarto: recapitulamos
Hemos visto, paso a paso, un ejemplo de cómo podemos utilizar el framework MVC de Microsoft para el desarrollo de aplicaciones web que hacen uso de Ajax, utilizando para ello la excelente librería de scripting jQuery.Para ello hemos creado una vista que es la página Web con el formulario principal, y otra vista parcial con el fragmento compuesto por el servidor con la información recibida. El controlador, por su parte, incluye acciones para responder a las peticiones del cliente independientemente de si se originan a través de Ajax o mediante la navegación del usuario, mostrando la vista oportuna.
La modificación dinámica del interfaz, así como las llamadas asíncronas al servidor, encajan perfectamente en la filosofía MVC teniendo en cuenta algunas reglas básicas, como el respeto a las responsabilidades de cada capa.
Y ahora, lo prometido:
Descargar proyecto (Visual Web Developer Express 2008).
Publicado en: www.variablenotfound.com.
Publicado por José M. Aguilar a las 10:05 p. m.
Etiquetas: ajax, asp.net, aspnetmvc, javascript, jquery
Recordemos que una parte de una clase parcial puede declarar un método y utilizarlo (invocarlo) dentro de su código; si el método está implementado en otro fragmento de la clase, se ejecutará como siempre, pero si no ha sido implementado, el compilador eliminará tanto la declaración del método como las llamadas que se hagan al mismo.
Sin embargo esta eliminación pueden causar efectos no deseados difíciles de detectar.
Veámoslo con un ejemplo. Supongamos una clase parcial como la siguiente, que representa a una variable de tipo entero que puede ser incrementada o decrementada a través de métodos:
public partial class Variable
{
partial void Log(string msg);
private int i = 0;
public void Inc()
{
i++;
Log("Incremento. I: " + i);
}
public void Dec()
{
Log("Decremento. I: " + (--i));
}
public int Value
{
get { return i; }
}
}
Creamos ahora un código que utiliza esta clase de forma muy simple: crea una variable, la incrementa dos veces, la decrementa una vez y muestra el resultado:
Variable v = new Variable();
v.Inc();
v.Inc();
v.Dec();
Console.WriteLine(v.Value);
Obviamente, tras la ejecución de este código la pantalla mostrará por consola un "1", ¿no? Claro, el resultado de realizar dos incrementos y un decremento sobre el cero.
Pues no necesariamente. De hecho, es imposible conocer, a la vista del código mostrado hasta ahora, cuál será el resultado mostrado por consola al finalizar la ejecución. Dependiendo de la existencia de la implementación del método parcial
Log()
, declarado e invocado en la clase Variable
anterior, puede ocurrir:- Si existe otra porción de la misma clase (otra
partial class Variable
) donde se implemente el método, se ejecutará éste. El valor mostrado por consola, salvo que desde esta implementación se modificara el valor del campo privado, sería "1". - Si no existe una implementación del método
Log()
en la clase, el compilador eliminará todas las llamadas al mismo. Pero si observáis, esto incluye el decremento del valor interno, que estaba en el interior de la llamada como un autodecremento:
Por tanto, en este caso, el compilador eliminará tanto la llamada aLog("Decremento. I: " + (--i));
Log()
como la operación que se realiza en el interior. Obviamente, el resultado de ejecución de la prueba anterior sería "2".
Lo mismo ocurriría si el resultado a mostrar fuera el valor de retorno de una llamada a otra función: esta no se ejecutaría, lo cual puede ser grave si en ella se realiza una operación importante, como por ejemplo:public function InicializaValores()
{
Log("Elementos reestablecidos: " + reseteaElementos() );
}
Por ejemplo, imaginad que el ejemplo anterior contiene una implementación de
Log()
; la aplicación funcionaría correctamente. Sin embargo, si pasado un tiempo se decide eliminar esta implementación (por ejemplo, porque ya no es necesario registrar las operaciones realizadas), la operación de decremento (Dec()
) dejaría de funcionar. Aunque, eso sí, no es nada que no se pueda solucionar con un buen juego de pruebas...
Publicado en: http://www.variablenotfound.com/.
Estos métodos, declarados en el contexto de una clase parcial, permiten comunicar de forma segura los distintos fragmentos de dicha clase. De forma similar a los eventos, permiten que un código incluya una llamada a una función que puede (o no) haber sido implementada por un código cliente, aunque en este caso obligatoriamente la implementación se encontrará en uno de los fragmentos de la misma clase desde donde se realiza la llamada.
En la práctica significa que una clase parcial puede declarar un método y utilizarlo (invocarlo) dentro de su código; si el método está implementado en otro fragmento de la clase, se ejecutará como siempre, pero si no ha sido implementado, el código correspondiente a la llamada será eliminado en tiempo compilación para optimizar el resultado... ¡sí, eliminado!
Por ejemplo, el siguiente código muestra una declaración de un método parcial en el interior de una clase, y su utilización desde dentro de uno de sus métodos:
// C#
public partial class Ejemplo
{
// El método parcial se declara
// sin implementación...
partial void Log(string msg);
public void RealizarAlgo()
{
hacerAlgoComplejo();
Log("¡Ojo!"); // Usamos el método
// parcial declarado antes
}
[...] // Otros métodos y propiedades
}
' VB.NET
Partial Public Class Ejemplo
' El método parcial se declara, sin
' implementar nada en el cuerpo
Partial Private Sub Log(ByVal msg As String)
End Sub
Public Sub RealizarAlgo()
HacerAlgoComplejo()
Log("¡Ojo!") ' Usamos el método parcial
End Sub
[...] ' Otros métodos y propiedades
End Class
Y esta la parte realmente curiosa. Cuando el compilador detecta la invocación del método parcial
Log()
, buscará en todos los fragmentos de la clase a ver si existe una implementación del mismo. Si no existe, eliminará del ensamblado resultante la llamada a dichos métodos, es decir, actuará como si éstas no existieran en el código fuente.En caso afirmativo, es decir, si existen implementaciones como las del siguiente ejemplo, todo se ejecutará conforme a lo previsto:
// C#
public partial class Ejemplo
{
partial void Log(string msg)
{
Console.WriteLine(msg);
}
}
' VB.Net
Partial Public Class Ejemplo
Private Sub Log(ByVal msg As String)
Console.WriteLine(msg)
End Sub
End Class
Antes comentaba que los métodos parciales son, en cierto sentido, similares a los eventos, pues conceptualmente permiten lo mismo: pasar el control a un código cliente en un momento dado, en el caso de que éste exista. De hecho, hay muchos desarrolladores que lo consideran como un sustituto ligero a los eventos, pues permite prácticamente lo mismo pero se implementan de forma más sencilla.
Existen, sin embargo, algunas diferencias entre ambos modelos, como:
- Los métodos parciales se implementan en la propia clase, mientras que los eventos pueden ser consumidos también desde cualquier otra
- El enlace, o la suscripción, a eventos es dinámica, se realiza en tiempo de ejecución, es necesario incluir código para ello; los métodos parciales, sin embargo, se vinculan en tiempo de compilación
- Los eventos permiten múltiples suscripciones, es decir, asociarles más de un código cliente
- Los eventos pueden presentar cualquier visibilidad (pública, privada...), mientras que los métodos parciales son obligatoriamente privados.
Por último, es conveniente citar algunas consideraciones sobre los métodos parciales:
- Deben ser siempre privados (ya lo había comentado antes)
- No deben devolver valores (en VB.Net serían
SUB
, en C# serían de tipovoid
) - Pueden ser estáticos (shared en VB)
- Pueden usar parámetros, acceder a los miembros privados de la clase... en definitiva, actuar como un método más de la misma
En resumen, los métodos parciales forman parte del conjunto de novedades de C# y VB.Net que no son absolutamente necesarias y que a veces pueden parecer incluso diabólicas, pues facilitan la dispersión de código y dificultan la legibilidad. Además, en breve publicaré un post comentando posibles efectos laterales a tener en cuenta cuando usemos los métodos parciales en nuestros desarrollos.
Sin embargo, es innegable que los métodos parciales nos facilitan enormemente la inclusión de código de usuario en el interior de clases generadas de forma automática. Por ejemplo, el diseñador visual del modelo de datos de LinqToSQL genera los
DataContext
como clases parciales, en las que define un método parcial llamado OnCreated()
. Si el usuario quiere incluir alguna inicialización personal al crear los DataContext, no tendrá que tocar el código generado de forma automática; simplemente creará otro fragmento de la clase parcial e implementará este método, de una forma mucho más natural y cómoda que si se tratara de un evento.Publicado en: http://www.variablenotfound.com/.
Publicado por José M. Aguilar a las 9:10 p. m.
Etiquetas: .net, c#, desarrollo, programación, vb.net, vs2008