tag:blogger.com,1999:blog-33985338642169345202024-03-05T14:18:01.957-08:00Aprendiendo.NETPablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.comBlogger32125tag:blogger.com,1999:blog-3398533864216934520.post-24867698072254220382016-09-28T12:46:00.000-07:002016-09-28T12:46:47.131-07:00Calcular un hash MD5 y devolverlo en formato leíbleEl uso de hashes es bastante común en programación, se puede utilizar para guardar una contraseña, (en este caso ya no es recomendable recurrir al MD5), se puede usar para guardar una cadena de verificación o firma e incluso para comparar archivos.<br />
<br />
El framework .Net nos presenta una gran variedad de herramientas para calcular un hash, en este caso usaremos la clase MD5CryptoServiceProvider del namespace using System.Security.Cryptography.<br />
<br />
Una duda que muchas veces aparece al intentar usar estos CryptoServiceProviders es cómo mostrar el hash para que sea leíble, ya que la función que utilizamos nos devuelveun Array de bytes.<br />
<br />
Pues eso es sencillo, recurrimos a la función <a href="https://msdn.microsoft.com/en-us/library/dwhawy9k(v=vs.110).aspx" target="_blank">ToString() y como argumento pasamos "x2"</a> con lo que por cada ítem del array obtendremos un valor hexadecimal de 2 dígitos.<br />
<br />
Entonces la función se vería así:<br />
<br />
<code>
</code>
<pre><code><span style="color: #0b5394;">public string</span> MD5Hash(<span style="color: #0b5394;">string </span>texto)
{
<span style="color: #0b5394;">var </span>resultado = <span style="color: #0b5394;">new </span><span style="color: #134f5c;">StringBuilder</span>();
<span style="color: #0b5394;">using </span>(<span style="color: #0b5394;">var </span>md5 = <span style="color: #0b5394;">new </span><span style="color: #134f5c;">MD5CryptoServiceProvider</span>())
{
<span style="color: #0b5394;">var </span>hash = md5.ComputeHash(System.Text.<span style="color: #134f5c;">Encoding</span>.UTF8.GetBytes(texto);
<span style="color: #0b5394;">foreach </span>(<span style="color: #0b5394;">byte </span>b <span style="color: #0b5394;">in </span>hash)
{
resultado.Append(b.ToString(<span style="color: #660000;">"x2"</span>));
}
}
<span style="color: #0b5394;">return </span>resultado.ToString();
}
</code></pre>
<code>
</code>Pablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.com0tag:blogger.com,1999:blog-3398533864216934520.post-43971085854199450072016-06-02T20:48:00.000-07:002016-06-02T20:48:11.166-07:00Password prompt en aplicación de consola de .NetMuchas veces utilizo aplicaciones de consola, principalmente cuando desarrollo un cliente para alguna API y necesito crear una aplicación de test para mi librería... pero algo que siempre me molestó es no poder enmascarar el password mientras se lo ingresa en la consola... así fue que un día decidí investigar y me encontré con una solución que tenía algunas fallas y le faltaba trabajo... pero recuerdo que lo tomé como base y luego de una buena edición lo uso en todos mis proyectos de aplicación de consola en los que se ingresa un password.<br />
<br />
Creo que una de las formas más seguras de enmascarar un password es el que se usa en GNU/Linux, no mostrar nada al ingresar el password, de esa forma no sólo se ocultan los caracteres que conforman el password, pero también se oculta el largo de la cadena, pero hay quienes prefieren ver un asterisco por cada caracter, así que eso es configurable.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi8HmDnpB66Ps44KFYgFEFDDTzk1nwgI_lHEQqbAjPDS2AGeatAY4HCiSNW7W2DV-cCaoIG4Xnl61I5Mc_M61j-eYJwXqyYXIcOTQ-7gjXHpq8wBPG6uav1xilhJLA5YzlZ0AnfkVwmH5g/s1600/enmascarar-passwd-console-csharp.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="168" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi8HmDnpB66Ps44KFYgFEFDDTzk1nwgI_lHEQqbAjPDS2AGeatAY4HCiSNW7W2DV-cCaoIG4Xnl61I5Mc_M61j-eYJwXqyYXIcOTQ-7gjXHpq8wBPG6uav1xilhJLA5YzlZ0AnfkVwmH5g/s320/enmascarar-passwd-console-csharp.png" width="320" /></a></div>
<div style="text-align: center;">
<br /></div>
<br />
Veamos la función que lee y devuelve el password:<br />
<br />
<pre><code><span style="color: blue;">static string</span> LeerPassword()
{
<span style="color: #38761d;">// Caracter a mostrar en pantalla </span>
<span style="color: blue;">const string</span> mascara = <span style="color: #990000;">"*"</span>;
<span style="color: blue;">ConsoleKeyInfo </span>key;
<span style="color: blue;">var </span>passwd = <span style="color: blue;">string</span>.Empty;
<span style="color: blue;">do</span>
{
<span style="color: #38761d;">// Leer una tecla a la vez</span>
key = <span style="color: blue;">Console</span>.ReadKey(<span style="color: blue;">true</span>);
<span style="color: blue;">if </span>(key.Key != <span style="color: blue;">ConsoleKey</span>.Backspace && key.Key != <span style="color: blue;">ConsoleKey</span>.Enter)
{
<span style="color: #38761d;">// Agrego el caracter a la cadena</span>
passwd += key.KeyChar;
<span style="color: blue;">Console</span>.Write(mascara);
}
<span style="color: blue;">else if</span> (key.Key == <span style="color: blue;">ConsoleKey</span>.Backspace)
{
<span style="color: blue;">if </span>(mascara.Length > 0)
{
<span style="color: #38761d;">// Si la máscara se imprime hay que borrarla</span>
<span style="color: blue;">Console</span>.Write("\b \b");
}
<span style="color: blue;">if </span>(!<span style="color: blue;">string</span>.IsNullOrEmpty(passwd))
{
<span style="color: #38761d;">// Quitar el último caracter de la cadena</span>
passwd = passwd.Substring(0, passwd.Length - 1);
}
}
}
<span style="color: blue;">while </span>(key.Key != <span style="color: blue;">ConsoleKey</span>.Enter);
<span style="color: blue;">return </span>passwd;
}
</code></pre>
<br />
<br />
Con esta simple función ya tenemos esta funcionalidad implementada, resta invocarla desde el punto donde queremos leer el password y listo!<br />
<pre><code>
<span style="color: blue;">static void</span> Main(<span style="color: blue;">string</span>[] args)
{
<span style="color: blue;">Console</span>.Clear();
<span style="color: blue;">Console</span>.WriteLine(<span style="color: #990000;">"Aprendiendo.Net - Enmascarar password"</span>);
<span style="color: blue;">Console</span>.WriteLine();
<span style="color: blue;">Console</span>.Write(<span style="color: #990000;">"Usuario: "</span>);
<span style="color: blue;">var </span>usuario = <span style="color: blue;">Console</span>.ReadLine();
<span style="color: blue;">Console</span>.Write(<span style="color: #990000;">"Password: "</span>);
<span style="color: blue;">var </span>passwd = LeerPassword();
<span style="color: #38761d;"> /*
* Validación y resto del código
*/</span>
<span style="color: blue;">Console</span>.ReadLine();
}
</code></pre>
<br />
<br />
Modificando el valor de la constante mascara en nuestra función LeerCodigo() podremos variar el caracter que se mostrará en pantalla al ingresar el password, incluso podemos usar un string vacío para lograr el efecto que nombraba respecto a las aplicaciones en GNU/Linux.Pablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.com0tag:blogger.com,1999:blog-3398533864216934520.post-80657043707635443542015-08-04T13:07:00.000-07:002015-08-04T13:07:13.015-07:00Entendiendo la estructura TimeSpan en C#Una instancia de la estructura TimeSpan representa un periodo de tiempo pero también nos brinda una serie de propiedades y métodos útiles.<br />
<br />
Para crear una instancia de TimeSpan tenemos varios constructores, por lo que deberemos buscar el más adecuado a nuestra necesidad.... podemos crear un TimeSpan a partir de <a href="http://aprendiendonet.blogspot.com/search/label/Ticks" target="_blank">ticks</a>, o usando el constructor que más parámetros acepta, podemos crearlo a partir de un número de días, horas, minutos, segundos y milisegundos.<br />
<br />
Otra forma de inicializar un TimeSpan es a partir se alguno de sus métodos estáticos cuyo nombre comienza con From, (<i>Desde </i>o <i>A partir</i> <i>de </i>en inglés), entonces podríamos inicializar untimeSpan a partir de un número de minutos usando <code>TimeSpan.FromMinutes(double)</code>.<br />
<br />
También podemos sumar y restar instancias de TimeSpan, pero como cada instancia es inmutable, el resultado de la operación lo asignaremos a otra variable.<br />
<br />
Para realizar estas operaciones recurriremos al los métodos de instancia Add y Subtract los cuales aceptan un único parámetro de tipo TimeSpan, vamos un ejemplo de cada operación:<br />
<br />
<code>
TimeSpan periodo1 = TimeSpan.FromHours(1);<br />
TimeSpan periodo2 = TimeSpan.FromHours(3);<br />
<br />
TimeSpan suma = periodo1.Add(periodo2); <span style="color: #274e13;">// suma representa un TimeSpan de 4 horas: 1 + 3</span><br />
<br />
TimeSpan resta = periodo2.Subtract(periodo1); <span style="color: #274e13;">// resta representa un TimeSpan de 2 horas, 3 - 1</span><br />
</code>
<br />
<br />
<br />
Algunas propiedades de TimeSpan son tan importantes como útiles a la hora de trabajar con intervalos de tiempo, me refiero a las propiedades que nos devuelven el valor de ese intervalo en cierta unidad, por ejemplo la cantidad de segundos totales correspondientes a ese periodo.<br />
Estas propiedades son nombradas como <i>TotalUnidadDeTiempo</i>, por ejemplo TotalSeconds o TotalMinutes, veamos un ejemplo:<br />
<br />
<code>
TimeSpan foo = TimeStamp.FromMinutes(2);<br />
Console.WriteLine(foo.TotalSeconds.ToString()); <span style="color: #274e13;">// Se imprimirá 120</span><br />
</code>
<br />
<br />
<br />
No debemos confundir estas propiedades con las que sólo reciben el nombre de la unidad de tiempo a la que representan, por ejemplo Hours o MilliSeconds, ya que éstas representan el valor de esa unidad, no la representación total del periodo en esa unidad.<br />
<br />
Por ejemplo:<br />
<br />
<code>
TimeSpan foo = TimeStamp.FromSeconds(70);<br />
Console.WriteLine(foo.TotalSeconds.ToString()); <span style="color: #274e13;">// Se imprimirá 70</span><br />
Console.WriteLine(foo.Seconds.ToString()); <span style="color: #274e13;">// Se imprimirá 10, ya que el TimeSpan representa un periodo de 1 minuto y 10 segundos</span>
</code>
<br />
<br />
<br />Pablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.com0tag:blogger.com,1999:blog-3398533864216934520.post-27877506041862743252015-04-07T12:52:00.000-07:002019-05-22T07:41:54.844-07:00Convertir Ticks en Fecha y viceversaHoy quisiera publicar un tip que es prácticamente tonto, pero dado que he recibido la consulta y la solución puede no resultar de lo más intuitiva, aquí dejo una función para convertir Ticks en Fecha y Fecha en Ticks.<br />
<br />
<br />
<b>Antes que nada, qué son Ticks?</b><br />
<br />
Un tick es el mínimo valor que representa un paso, equivalente a 100 nanosegundos, o sea en un mili segundo hay 10.000 ticks.<br />
<br />
<br />
<pre><code>
<span style="color: blue;">Public Function</span> TicksToDateTime(ticks <span style="color: #073763;">As </span><span style="color: blue;">Long</span>) <span style="color: #073763;">As </span><span style="color: blue;">DateTime</span>
</code><span style="color: #6aa84f;">' Intuitivo sería tener una función DateTime.FromTicks(Long)</span></pre>
<pre><code><span style="color: #073763;"> </span><span style="color: blue;">Return </span><span style="color: #073763;">New </span><span style="color: blue;">DateTime</span>(ticks)
<span style="color: blue;">End Function</span>
<span style="color: blue;">Public Function</span> DateTimeToTicks(date <span style="color: #073763;">As </span><span style="color: blue;">DateTime</span>) <span style="color: #073763;">As </span><span style="color: blue;">Long</span>
<span style="color: blue;"><span style="color: #073763;">Return</span> </span>date.Ticks
<span style="color: blue;">End Function</span>
</code></pre>
<br />
<br />
Como había anticipado, la solución es muy tonta!<br />
<br />
En cuanto a tener una forma más intuitiva, podríamos crear un <a href="http://aprendiendonet.blogspot.com/search/label/Extension%20Methods" target="_blank">Método de Extensión</a>, (Extension Method), que afecte la clase DateTime, de esa manera tener <code>DateTime.FromTicks(Long)</code>.<br />
<br />
<br />Pablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.com1tag:blogger.com,1999:blog-3398533864216934520.post-61821460586222539202013-11-19T04:50:00.001-08:002013-11-19T04:50:36.932-08:00Trace, la herramienta versátil para crear logsAlgo que siempre necesitamos es llevar líneas de log para saber qué está ocurriendo con nuestras aplicaciones y esto se vuelve prácticamente indispensable al trabajar con <a href="http://aprendiendonet.blogspot.com/search/label/Servicios%20de%20Windows" target="_blank">Windows Services o Servicios de Windows</a> ya que al no tener UI se hace muy difícil saber qué ocurre...<br />
<br />
Afortunadamente encontramos la clase Trace en el namespace <a href="http://aprendiendonet.blogspot.com/search/label/System.Diagnostics" target="_blank">System.Diagnostics</a>, la cual nos permite de manera muy sencilla ir dejando líneas de log en nuestro código, veamos un ejemplo simple:<br />
<br />
<pre><code>
<span style="color: blue;">Public Sub New()</span>
System.Diagnostics.Trace.WriteLine("Inicio de constructor")
<span style="color: #6aa84f;">' Proceso de datos</span>
System.Diagnostics.Trace.WriteLine("Fin de constructor")
<span style="color: blue;">End Sub</span>
</code>
</pre>
Entonces para el propósito de este post lo que nos interesa es básicamente la capacidad de escribir de esta clase, para lo cual utilizaremos 2 métodos: Write() y WriteLine() cuya principal diferencia, como lo indica su nombre, es que uno escribe y el otro escribe y termina la línea.<br />
<br />
Entonces ya sabemos como utilizar esta herramienta para escribir líneas de log, nos resta leer nuestro log, para lo cual Visual Studio cuenta con una ventana llamada Output o Salida (CTRL + W + O) en la cual se van imprimiendo las líneas de nuestro log... impecable si estamos trabajando en nuestro entorno de desarrollo, pero si debemos leer nuestro log desde una aplicación ya compilada (en modo Debug) podemos utilizar la herramienta <a href="http://technet.microsoft.com/en-us/sysinternals/bb896647.aspx" target="_blank">Debug View</a> que nos permite tener una ventana con nuestro log en la cual podremos usar filtros, resaltado de colores, etc.<br />
<br />
<br />Pablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.com0tag:blogger.com,1999:blog-3398533864216934520.post-36238070615627567602012-07-23T17:11:00.001-07:002013-11-21T00:39:54.722-08:00SQL Server Error 4064 - La soluciónSi bien éste no es un tip de .Net, sino que es de SQL Server, me pareció útil compartirlo.
<br />
<blockquote class="tr_bq">
No se puede abrir la base de datos predeterminada del usuario. Error de inicio de sesión.</blockquote>
<br />
Este error ocurre cuando intentamos iniciar sesión en SQL Server con un usuario cuya base de datos por defecto fue eliminada del servidor.<br />
En un servidor muy cerrado en cuanto a su seguridad, este problema puede dar la impresión de ser muy dificil de resolver, y más si se trata del único usuario con el que tenemos acceso al servidor.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiCyHg8BnXnIyNmPZa1Y3cD_MZ9Nt9C95lXRg4y7urG0f1e-luacGuz4D-OcRiG5h7T6EQoInBO577RVrjEFV_4PCrWS9S833P3vW0iFL1GApJIBeKpjIF81MTb9R4ZcWPvK8jWBPlkz0s/s1600/sql-server-error-4064.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="165" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiCyHg8BnXnIyNmPZa1Y3cD_MZ9Nt9C95lXRg4y7urG0f1e-luacGuz4D-OcRiG5h7T6EQoInBO577RVrjEFV_4PCrWS9S833P3vW0iFL1GApJIBeKpjIF81MTb9R4ZcWPvK8jWBPlkz0s/s320/sql-server-error-4064.png" width="320" /></a></div>
<br />
Sin embargo la solución en muy sencilla, en el SQL Server Management Studio, (Express o no), buscamos en el diálogo de inicio de sesión el botón "Opciones" con lo cual accederemos a las opciones de la conexión que deseamos abrir.<br />
<br />
A continación nos ubicamos en la pestaña "Propiedades de la conexión" y buscamos el campo "Conectar con base de datos", el cual mostrará el valor <predeterminado>, borramos ese valor y escribimos master, (o cualquier nombre válido de base de datos a la cual tengamos acceso en el servidor), hacemos click en el botón Conectar y listo!</predeterminado><br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhoS6l1CeNTUX1c7vqKFECw1NOn1TV4WdLQC67VNWRyjngRFhH7vit3e4ucDA8IPoZ7dPJHpyZunpkA-B57HT685MZU-sDi2ATHT6SFyoLYkKjjXhv_Eus7VIzIo_m-8CxUScd1TiRWtno/s1600/sql-server-error-4064.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhoS6l1CeNTUX1c7vqKFECw1NOn1TV4WdLQC67VNWRyjngRFhH7vit3e4ucDA8IPoZ7dPJHpyZunpkA-B57HT685MZU-sDi2ATHT6SFyoLYkKjjXhv_Eus7VIzIo_m-8CxUScd1TiRWtno/s320/sql-server-error-4064.png" width="272" /></a></div>
<br />Pablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.com17tag:blogger.com,1999:blog-3398533864216934520.post-68370430828418363472012-07-07T14:32:00.003-07:002012-07-07T14:32:52.528-07:00Tip: Cómo saber cuántos días tiene un año en .Net?Algunos programadores no lo tienen en cuenta, otros simplemente nunca lo necesitaron y otros no lo pensaron hasta que se chocaron con el problema... pero es un tip que puede ser útil, Cómo saber cuántos días tiene un año?<br />
<br />
La respuesta se encuentra en la clase <a href="http://msdn.microsoft.com/es-es/library/system.globalization.calendar.aspx" target="_blank">Calendar</a>, del namespace System.Globalization, la cual contiene varios métodos y funciones de ayuda para operaciones de tiempo, pero la que nos importa en este momento es <a href="http://msdn.microsoft.com/es-es/library/017twd3b.aspx" target="_blank">Calendar.GetDaysInYear(int Year)</a>.<br />
<br />
Su uso? Depende de lo que necesitemos, pero a mi entender lo más lógico es utilizar la instancia de Calendar que se encuentra instanciada en nuestra CurrentCulture, por lo que se utilizaría de la siguiente manera:<br />
<br />
<br />
En <a href="http://aprendiendonet.blogspot.com/search/label/VB.Net">VB.Net</a>:<br />
<br />
<code>Dim DiasDelAnio As Integer = CultureInfo.CurrentCulture.Calendar.GetDaysInYear(aux.Year)</code><br />
<br />
<br />
En <a href="http://aprendiendonet.blogspot.com/search/label/C%23">C#</a><br />
<br />
<code>int diasDelAnio = CultureInfo.CurrentCulture.Calendar.GetDaysInYear(aux.Year)</code>Pablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.com0tag:blogger.com,1999:blog-3398533864216934520.post-1621419478562038672010-09-17T06:38:00.000-07:002010-09-17T06:58:06.691-07:00Como hacer un servicio de Windows auto instalableTodos nos hemos encontrado en la situación de tener que instalar un servicio, y utilizar el comando <strong>installutil.exe</strong>, pero por qué no ahorrarnos un paso y hacer que el servicio sea auto instalable?<br /><br /><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi7AIWReb_nzmt41eEY42eePdtK2_yHjoevxPRBltu1ZXWNwoZk0HWokeVuUe9rYSkUdEZtrS3WFmDpa-shKsWR9xhIkjGur5L3SDqT4F3HKTtfSp55vLdZOFKHx4X2rW0bE9Dhdl5kFSc/s288/servicios-de-windows.png" style="text-align:center" /><br /><br />Este tip no tiene mayor secreto, simplemente es <strong>un atajo al installutil.exe dentro del ejecutable de nuestro servicio</strong>.<br /><br />Lo que haremos será ir a la clase Program.cs en C# o Módulo Main.vb eb VB y modificar el método main de la siguiente manera:<br /><br /><pre><br />static class Program<br /> {<br /> /// <summary><br /> /// The main entry point for the application.<br /> /// </summary><br /> static void Main(String[] args)<br /> {<br /> if (args.Length > 0)<br /> {<br /> switch (args[0])<br /> {<br /> case "/install":<br /> install();<br /> break;<br /> case "/uninstall":<br /> uninstall();<br /> break;<br /> }<br /> }<br /> else<br /> {<br /> ServiceBase[] ServicesToRun;<br /> ServicesToRun = new ServiceBase[] { new SecurityProviderService() };<br /> ServiceBase.Run(ServicesToRun);<br /> }<br /> }<br /> }<br /></pre><br /><br />Lo que hicimos fue agregarle a nuestro ejecutable la posibilidad de recibir parámetros, en éste caso /install o /uninstall, sólo nos resta crear los métodos para ejecutar éstas acciones y listo.<br /><br /><pre><br /> // Devuelve la ruta a installutil.exe<br /> private static String installUtill()<br /> {<br /> return System.IO.Path.Combine(System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory(), "installutil.exe");<br /> }<br /><br /> private static void uninstall()<br /> {<br /> if (System.IO.File.Exists(installUtill()))<br /> {<br /> System.Diagnostics.Process.Start(installUtill(), String.Format("/u \"{0}\"", System.Reflection.Assembly.GetExecutingAssembly().Location));<br /> System.Diagnostics.Trace.WriteLine("Service uninstalled.");<br /> }<br /> else<br /> throw new System.IO.FileNotFoundException("InstallUtil.exe not found");<br /> }<br /><br /> private static void install()<br /> {<br /> if (System.IO.File.Exists(installUtill()))<br /> {<br /> System.Diagnostics.Process.Start(installUtill(), String.Format("\"{0}\"", System.Reflection.Assembly.GetExecutingAssembly().Location));<br /> System.Diagnostics.Trace.WriteLine("Service installed.");<br /> }<br /> else<br /> throw new System.IO.FileNotFoundException("InstallUtil.exe not found");<br /> }<br /></pre><br /><br />Muy simple, ahora cuando necesitemos instalar nuestro servicio simplemente lo haremos desde una consola ejecutando <code>NombreDelServicio.exe /install</code>Pablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.com2tag:blogger.com,1999:blog-3398533864216934520.post-8356854360357252382010-08-16T05:29:00.000-07:002010-08-16T05:39:06.025-07:00Referencia de atajos de teclado para Visual Studio 2010Desde la web de Microsoft, nos ofrecen una serie de archivos PDF con los <strong>atajos de teclado para Visual Studio 2010</strong>.<br /><br /><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgd5xLUSNihoWQU_4h0b2L2E_RCPJtDjXly8XuPNgaSzsEjlVF9kxy2_a_QsIKkiZEi68OAgtMiq0rkcd8a7eFPKylyUz5WhK6DsL1XyOBvb_jBDVYCdYpxy03UwpXi25yh5DjlKSoPCWU/s1600/VS-2010-CSharp-Shortcuts.jpg"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 320px; height: 247px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgd5xLUSNihoWQU_4h0b2L2E_RCPJtDjXly8XuPNgaSzsEjlVF9kxy2_a_QsIKkiZEi68OAgtMiq0rkcd8a7eFPKylyUz5WhK6DsL1XyOBvb_jBDVYCdYpxy03UwpXi25yh5DjlKSoPCWU/s320/VS-2010-CSharp-Shortcuts.jpg" border="0" alt="C# shortcuts para Visual Studio 2010" id="BLOGGER_PHOTO_ID_5505986036402555714" /></a><br /><br /><a href="http://www.microsoft.com/downloads/details.aspx?FamilyID=92CED922-D505-457A-8C9C-84036160639F&displaylang=en" target="_blank" title="Atajos de teclado para Visual Studio 2010">Descargar</a>Pablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.com2tag:blogger.com,1999:blog-3398533864216934520.post-76490882600096430632009-10-14T11:24:00.000-07:002009-10-14T11:51:10.766-07:00Como acceder a la propiedad Text de un control NumericUpDown<img style="display:block; margin:0px auto 10px; text-align:center;width: 314px; height: 152px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjv-jiMXBauq9TJIX8PHmaCabRYJc72M7DHBMr4SOA45Etj7YCfmtMOHw90TuEB5WeUDTC0KfjNF4-guaKSaOP4pHoDoJVyuD2YqvkqVH2NJ8WjJpXK5J6SvhkFopxmdxxqF0IHC0WfaCQ/s320/NumericUpDown-problema-propiedad-Text.png" border="0" alt="Problema con NumericUpDown y la propiedad Text"id="BLOGGER_PHOTO_ID_5392529529207856610" /><br /><br />Hoy me encontré con un problemita trabajando sobre un formulario que contiene varios <strong>NumericUpDown</strong>. El tema es que se podía seleccionar un valor, luego borrarlo y el sistema no hacía una validación que indicara al usuario que debía introducir un valor.<br /><br />Al empezar a hacer pruebas noté que el texto queda vacío, pero el valor no cambia, o sea que si el <strong>NumericUpDown</strong> tiene un valor de 75 y le borramos el texto, al acceder a la propiedad Value del mismo, ésta nos devuelve 75.<br /><br />El problema para solucionar ese bug me lo encontré cuando intenté acceder a la propiedad Text, para chequear que la misma no contenga un String vacío... Oh sorpresa, no se puede acceder a esa propiedad porque es privada!!<br /><br />Para acceder a la propiedad Text aprovecharemos de la herencia de esta clase, <strong>NumericUpDown</strong> es una subclase de <strong>Control</strong>, entonces al hacer un cast podemos acceder a la propiedad Text para hacer el chequeo.<br />Esto nos permite varias soluciones posibles, una que me pareció bastante elegante fue asignar un event handler, o manejador de eventos al evento TextChanged, al cual también accederemos mediante un cast a <strong>Control</strong>.<br /><br /><br />Solución:<br /><br /><pre><code><br />Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load<br /> For Each c As Control In Me.Controls<br /> If TypeOf c Is NumericUpDown Then<br /> AddHandler c.TextChanged, AddressOf NumericUpDownControls_TextChanged<br /> End If<br /> Next<br />End Sub<br /><br />Private Sub NumericUpDownControls_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs)<br /> If CType(sender, Control).Text = String.Empty Then<br /> CType(sender, Control).Text = CType(sender, NumericUpDown).Value.ToString()<br /> End If<br />End Sub<br /></code></pre><br /><br /><br />Otra posible solución, más eficiente desde el punto de vista de reusabilidad sería crear un control que herede de <strong>NumericUpDown</strong> y ya implemente de por si este comportamiento; que a mi entender es una falta de parte del equipo que desarrolló el control.Pablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.com3tag:blogger.com,1999:blog-3398533864216934520.post-87328311395275232422009-06-08T07:21:00.000-07:002009-06-08T07:28:41.681-07:00Deshabilitar el Cortar / Copiar de líneas vacías en Visual StudioUn comportamiento muy molesto de <strong>Visual Studio</strong> es cuando por error copias o cortas una línea vacía, o sin seleccionar nada... hasta hace un tiempo pensaba que era el comportamiento normal del <strong>IDE</strong>, pero un día hurgando en la <strong>ventana de opciones</strong> me encontré con que se puede desactivar.<br /><br /><img style="display:block; margin:0px auto 10px; text-align:center;width: 400px; height: 237px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgeAKdamtbiPJhS7quDTPqRGEUofzhqRe25ba3R2ueAS4utUS1KJVRaInrlHxjKQHizoXE6PcdIsM0w_UXpfhwiaymKjR5B64044oN15cLPZHMkXMkTtE3kpCJHBCRhUDyrAGk6crjFYWk/s400/visual-studio-no-copiar-lineas-vacias.jpg" border="0" alt="Evitar copiar sin seleccionar en Visual Studio"id="BLOGGER_PHOTO_ID_5344962329128686178" />Pablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.com0tag:blogger.com,1999:blog-3398533864216934520.post-64271617151935560522009-05-07T11:39:00.000-07:002012-07-28T01:57:23.897-07:00Extendiendo la clase List(Of T)Hoy necesité <b>exportar el contenido de una Lista a un DataTable</b>, entonces pensé:<br />
<br />
<ol><br />
<li>Crear una tabla con la estructura necesaria</li>
<br />
<li>Recorrer la lista secuencialmente y agregar filas a la tabla</li>
</ol>
<br />
<br />
Pero pensando como programador, se me ocurrió que seguramente volviera a necesitar esta función y posiblemente la necesitaría con diferentes tipos de listas.<br />
<br />
Como era lógico, pensé en <b>Generics</b>, entonces me creé una función que recibe una <b>System.Collections.Generic.List(Of T)</b> y devuelve un <b>System.Data.DataTable</b> con la siguiente firma:<br />
<br />
<code>Public Function ListToDataTable(Of T)(List As System.Collections.Generic.List(Of T)) As System.Data.DataTable</code><br />
<br />
después creé toda la lógica utilizando <b>System.Reflection</b> para examinar las propiedades, y aprovechando los <b>CustomAttributes</b> para poder ocultar propiedades, entonces para hacer un Test, hice una clase con la siguiente estructura:<br />
<br />
<code></code><br />
<pre><code>
Public Class MiClase
Private mId As Integer
Private mNombre As String
Private mCantidad As Integer
<System.ComponentModel.Browsable(False)> _
Public Property Id() As Integer
Get
Return mId
End Get
Set(ByVal value As Integer)
mId = value
End Set
End Property
Public Property Nombre() As String
Get
Return mNombre
End Get
Set(ByVal value As String)
mNombre = value
End Set
End Property
Public Property Cantidad() As Integer
Get
Return mCantidad
End Get
Set(ByVal value As Integer)
mCantidad = value
End Set
End Property
Public Sub New()
End Sub
Public Sub New(ByVal Id As Integer, ByVal Nombre As String, ByVal Cantidad As Integer)
mId = Id
mNombre = Nombre
mCantidad = Cantidad
End Sub
End Class</code></pre>
<br />
<br />
como ven, la propiedad Id tiene un CustomAttribute, el <b>System.ComponentModel.BrowsableAttribute</b> seteado con el valor False, o sea que esa propiedad estaría excluida de la exportación.<br />
<br />
Todo muy lindo, funcionó a la perfección... pero se me ocurrió algo mejor... usar <b>Extension Methods</b> o <b>métodos de extensión</b>, esa nueva posibilidad que nos brinda el .Net Framework 3.5 de extender clases sin la necesidad de tener el código fuente de la misma.<br />
<br />
<img alt="Usando extension methods sobre la clase System.Collections.Generic.List(Of T)" border="0" id="BLOGGER_PHOTO_ID_5333154772841639090" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhQ4qJKj4p7vGXhwxSGdR2N1YT1xQ0laQqgj9FiHR8v2gttIrncqXxWlb3FPXfT47Dyhkm8LdPKGn7QCBPeldbhYHrnRBbNY0IhZolqKRXtle0XIJPMksX2HqYcqcaS_BVA8vASad0kr-4/s320/generic-list-extension-methods.png" style="display: block; height: 200px; margin: 0px auto 10px; text-align: center; width: 320px;" /><br />
<br />
Entonces creé un módulo llamado ListExtension y le asigné el mismo Namespace de la lista, <b>System.Collections.Generic</b>.<br />
<br />
<code></code><br />
<pre><code>
Imports System.Reflection
Imports System.ComponentModel
Imports System.Runtime.CompilerServices
Namespace System.Collections.Generic
Module ListExtension
''' <summary>
''' Gets a Datatable with all Browsable properties of T as columns containing all Items in the List.
''' </summary>
''' <typeparam name="T"></typeparam>
''' <param name="List">System.Collections.Generic.List(Of T)</param>
''' <returns>System.Data.DataTable</returns>
''' <remarks>http://aprendiendonet.blogspot.com</remarks>
<Extension()> _
Public Function ToDataTable(Of T)(ByVal List As List(Of T)) As DataTable
Dim dt As New DataTable()
Dim tipo As Type = GetType(T)
Dim members As MemberInfo() = tipo.GetMembers() ' Obtenemos todos los Miembros de la clase correspondiente al tipo T
For Each m As MemberInfo In members
If m.MemberType = MemberTypes.Property Then ' Sólo nos interesan las propiedades
Dim skip As Boolean = False
Dim atts As Object() = m.GetCustomAttributes(GetType(BrowsableAttribute), False) ' Chequeamos si tiene BrowsableAttribute
If atts.Length > 0 Then
If CType(atts(0), BrowsableAttribute).Browsable = False Then
skip = True ' Seteamos un flag para no agregar la columna
End If
End If
If Not skip Then
Dim c As DataColumn = Nothing
Try
c = New DataColumn(m.Name, CType(m, PropertyInfo).PropertyType) ' Nueva columna con el nombre de la propiedad y el tipo de la misma
Catch ex As Exception
c = New DataColumn(m.Name, GetType(String)) ' En caso de error intento crearla como String
End Try
dt.Columns.Add(c)
End If
End If
Next
For Each itm As T In List ' Recorro la lista y agrego una fila por cada item de la misma
Dim r As DataRow = dt.NewRow()
For Each c As DataColumn In r.Table.Columns
Dim aux As MemberInfo() = tipo.GetMember(c.ColumnName)
If aux.Length > 0 Then
r(c.ColumnName) = CType(aux(0), PropertyInfo).GetValue(itm, Nothing)
End If
Next
dt.Rows.Add(r)
Next
Return dt
End Function
End Module
End Namespace</code></pre>
<br />
<br />
Entonces ahora en toda mi solución tengo la posibilidad de exportar mis Listas a DataTable con tan solo llamar a este <b>método de extensión</b>:<br />
<br />
<code>Dim auxDT as DataTable = miLista.ToDataTable()</code>Pablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.com1tag:blogger.com,1999:blog-3398533864216934520.post-70676563206526591762009-04-29T12:49:00.000-07:002009-04-29T12:55:10.595-07:00Como dejar un log en el Visor de Sucesos de Windows<img style="display:block; margin:0px auto 10px; text-align:center;width: 400px; height: 195px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjr7tPUbF98RXdEjHjlkH_wWwZektpcC9ydEd_QcfWrvJs8MkUZhHNQ8D10MFZccAAFrRPx-NJF1_sXic0wxQX1QTZcJ76nlzFk57S8dQ3Ut_Ef2-synUxhDtlvwFY0nXFXilF7p_Z1rb0/s400/visor-de-sucesos-vbnet.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5330203267472789922" /><br />Todos hemos visto el <strong>Visor de Sucesos</strong> de Windows, y a veces es una buena alternativa para guardar registros de nuestras aplicaciones, ya que nos evita el tener que definirnos una estructura de logs.<br /><br />Suele ser muy útil cuando desarrollamos un <strong>Servicio de Windows</strong> y queremos llevar registro de excepciones o ciertas situaciones... por eso el ejemplo de hoy se trata de crear un log para nuestra aplicación utilizando la clase <strong>EventLog</strong> del Namespace <strong>System.Diagnostics</strong>.<br /><br /><code><pre><br /> Public Sub EscribirEventLog(ByVal Origen As String, ByVal Mensaje As String, ByVal Equipo As String, ByVal TipoDeEntrada As EventTipoDeEntrada)<br /> If Not EventLog.SourceExists(Origen, Equipo) Then ' Si el origen no existe lo creamos<br /> EventLog.CreateEventSource(Origen, "GNSys", Equipo)<br /> End If<br /><br /> Dim eLog As New EventLog("GNSys", Equipo, Origen) ' Instanciamos la clase EventLog<br /> eLog.WriteEntry(Mensaje, TipoDeEntrada, GetSafeShort(111), GetSafeShort(1)) ' Escribimos la entrada<br /> End Sub<br /><br /></pre></code><br /><br /><br />En el código anterior, (que por cierto es muy sencillo), en la línea que escribe nuestra entrada de log hay datos que deberemos definir en función de nuestras necesidades, veamos la línea y cómo está formada:<br /><br /><code>eLog.WriteEntry([Event], LogEntryType, GetSafeShort(111), GetSafeShort(1))</code><br /><br />La firma del método <code>WriteEntry</code> es la siguiente:<br /><br /><code>Public Shared Sub WriteEntry(ByVal source As String, ByVal message As String, ByVal type As System.Diagnostics.EventLogEntryType, ByVal eventID As Integer, ByVal category As Short)</code><br /><br />Viendo la siguiente imágen se puede ver qué es cada campo.<br /><br /><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg0wV_o1jhY9VPFudaoABtTiKtL3LUsmJMWVug_IthT1Txe6_TG151thC9FddtDP1cDMjXoAqihq7LxHennsrnEBIqnW-fKQm-lyRVX0s52VPkrQr72a8FkpnXAP40arUc1zLDqIRkiK7s/s1600-h/detalles-de-eventlog-vbnet.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 396px; height: 400px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg0wV_o1jhY9VPFudaoABtTiKtL3LUsmJMWVug_IthT1Txe6_TG151thC9FddtDP1cDMjXoAqihq7LxHennsrnEBIqnW-fKQm-lyRVX0s52VPkrQr72a8FkpnXAP40arUc1zLDqIRkiK7s/s400/detalles-de-eventlog-vbnet.png" border="0" alt="Detalles de una entrada en el Visor de Sucesos"id="BLOGGER_PHOTO_ID_5330203269434243826" /></a>Pablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.com1tag:blogger.com,1999:blog-3398533864216934520.post-24205982613737889652009-04-28T09:51:00.000-07:002009-04-28T10:00:32.455-07:00Como leer y escribir en el Registro de Windows desde una aplicación .NETPara trabajar sobre el registro de Windows utilizaremos las cases <strong>Registry</strong> y <strong>RegistryKey</strong> del <strong>Namespace Microsoft.Win32</strong>. Con éstas 2 clases podremos realizar todas tareas de lectura y escritura en el registro.<br />Este es un ejemplo muy sencillo, por lo tanto vamos a crear una clave <em>Key</em>, dentro crearemos un valor <em>Value</em> luego los eliminaremos. Después leeremos todos los <em>Values</em> que se encuentren en <code>HKLM\Microsoft\Windows\CurrentVersion\Run</code>, que son los que indican qué aplicaciones se ejecutan al iniciar Windows para todos los usuarios.<br /><br />Debajo de estas líneas está el código, uno de los ejemplos más sencillos para <strong>leer y escribir en el Registro de Windows</strong>.<br /><br /><img style="display:block; margin:0px auto 10px; text-align:center;width: 320px; height: 186px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj6O7cpJTb98pMScWD3WSnnhbCL-pDYcdoBkp6AH2SBR4eVrRe7bpc3n5v5f5T3UqxofRYPcV8cpza-XUoiKqdtTMqy6rfFzlyTis9afqapwosYWf20EGSKFnbXcp7dCuyj4olZd7Un1xc/s320/trabajar-registro-windows-NET.jpg" border="0" alt="Trabajar con el Registro de Windows en nuestra aplicación .NET"id="BLOGGER_PHOTO_ID_5329786466385630098" /><br /><br /><code><pre><br />Imports Microsoft.Win32<br />Public Class Form1<br /><br /> Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click<br /> CrearKey()<br /> End Sub<br /><br /> Private Sub CrearKey()<br /> Dim KeyPath As String = "Software\Test"<br /> Registry.CurrentUser.CreateSubKey(KeyPath)<br /> End Sub<br /><br /><br /> Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click<br /> CrearValue()<br /> End Sub<br /><br /> Private Sub CrearValue()<br /> Dim KeyPath As String = "Software\Test"<br /> Dim ValueName As String = "TestValue"<br /><br /> Dim key As RegistryKey = Registry.CurrentUser.OpenSubKey(KeyPath, True) ' True indica que se abre para escritura<br /> If key IsNot Nothing Then ' Si key es Nothing significa que no se encontró<br /> key.SetValue(ValueName, "Esto es una prueba", RegistryValueKind.String)<br /> Else<br /> If MessageBox.Show(String.Format("No se encontró la clave 'HKCU\{0}'. Desea crearla?", KeyPath), "", MessageBoxButtons.YesNo, MessageBoxIcon.Question) = Windows.Forms.DialogResult.Yes Then<br /> CrearKey() ' Creamos la clave y volvemos a intentar crear el valor<br /> CrearValue()<br /> End If<br /> End If<br /> End Sub<br /><br /><br /> Private Sub Button3_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button3.Click<br /> EliminarValue()<br /> End Sub<br /><br /> Private Sub EliminarValue()<br /> Dim KeyPath As String = "Software\Test"<br /> Dim ValueName As String = "TestValue"<br /><br /> Dim Key As RegistryKey = Registry.CurrentUser.OpenSubKey(KeyPath, True)<br /><br /> If Key IsNot Nothing Then<br /> If Key.GetValueNames().Contains(ValueName) Then ' Buscamos el nombre del valor en la lista de todos los valores de la clave<br /> Key.DeleteValue(ValueName) ' Borramos el valor<br /> Else<br /> MessageBox.Show(String.Format("No se encontró el valor '{0}'.", ValueName))<br /> End If<br /> Else<br /> MessageBox.Show(String.Format("No se encontró la clave 'HKCU\{0}'.", KeyPath))<br /> End If<br /> End Sub<br /><br /> Private Sub Button4_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button4.Click<br /> EliminarKey()<br /> End Sub<br /><br /> Private Sub EliminarKey()<br /> Dim KeyPath As String = "Software\Test"<br /><br /> Dim key As RegistryKey = Registry.CurrentUser.OpenSubKey(KeyPath)<br /><br /> If key IsNot Nothing Then<br /> Registry.CurrentUser.DeleteSubKey(KeyPath) ' Borramos la sub clave<br /> Else<br /> MessageBox.Show(String.Format("No se encontró la clave 'HKCU\{0}'.", KeyPath))<br /> End If<br /> End Sub<br /><br /><br /> Private Sub Button5_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button5.Click<br /> LeerRegistro()<br /> End Sub<br /><br /> Private Sub LeerRegistro()<br /> Dim KeyPath As String = "Software\Microsoft\Windows\CurrentVersion\Run"<br /> Dim key As RegistryKey = Registry.LocalMachine.OpenSubKey(KeyPath, False) ' Abrimos para sólo lectura<br /><br /> If key IsNot Nothing Then<br /> Dim sb As New System.Text.StringBuilder()<br /><br /> Dim values As String() = key.GetValueNames() ' Obtenemos los nombres de todos los valores en la key<br /> For Each value As String In values<br /> sb.AppendLine(String.Format("{0} > {1} ({2})", value, key.GetValue(value), key.GetValueKind(value).ToString()))<br /> Next<br /><br /> Me.TextBox1.Text = sb.ToString() ' Mostramos el resultado en nuestra TextBox Multilínea<br /> End If<br /> End Sub<br />End Class<br /></pre></code><br /><br /><img style="display:block; margin:0px auto 10px; text-align:center;width: 320px; height: 225px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEizib15FXSJ-tKaTdV6AVF6HRx3Uhp64KWcG-9vA0Oj5UE1lO_f6MAKVbbwc3lOdHiB8ssoD_L5lv7VkJejreJC0wRN_W5d3l2wrFZukLQEvLT2bf7pdWYq6uGHI0BxXAPBFFDN4duKFIo/s320/registry-create-value.png" border="0" alt="Registro de Windows - Aprendiendo.NET"id="BLOGGER_PHOTO_ID_5329786989078178258" />Pablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.com1tag:blogger.com,1999:blog-3398533864216934520.post-27040368159817442882009-03-09T06:19:00.000-07:002009-03-09T06:34:31.946-07:00Como imprimir un formulario - Microsoft.VisualBasic.PowerPacks.Printing.PrintFormEste código muestra una forma sencilla de <strong>imprimir un formulario</strong>, para lo cual utilizaremos la clase <strong>PrintForm</strong>, que se encuentra en el <strong>Namespace Microsoft.VisualBasic.PowerPacks.Printing</strong>.<br /><br />Lo primero que haremos será chequear si tenemos la referencia, para lo cual haremos click con el botón derecho sobre nuestro cuadro de herramientas y en el menú seleccionaremos la opción <em>Choose Items...</em>, cuando se abra el diálogo para seleccionar las referencias buscaremos la que corresponde al namespace antes mencionado y si no está seleccionada la seleccionaremos en este momento.<br /><br /><img style="display:block; margin:0px auto 10px; text-align:center; cursor:hand;width: 320px; height: 159px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgj8e1Zp5dmWR-2LQWJl-Y7Cr4LJ6RDH4DayRtLNEDxxgf4ZAh6xSkU7vKLq3CmmKI6mxUInOYyboFUKN9XMjmSkgSR1xNb-alB69iPw-UivnFMLGui49EGcY3I0QJoWmYhmbaMgvk2mOY/s320/ms-visualbasic-powerpacks-printing-printform.png" border="0" alt="Microsoft.VisualBasic.PowerPacks.Printing.PrintForm" id="BLOGGER_PHOTO_ID_5311179465267919090" /><br /><br />Puede ocurrir que no se muestre en el cuadro de herramientas, para poder verla haremos un click con el botón derecho sobre el cuadro de herramientas y en el menú seleccionaremos la opción <em>Show All</em>, que nos agregará varias secciones al cuadro de herramientas, la que buscamos se encuentra bajo el título de <strong>VisualBasic PowerPacks</strong>.<br /><br />Ahora ya podemos arrastrar este componente a nuestro form y crear el siguiente método para poder imprimir el formulario:<br /><br /><pre> Private Sub Imprimir()<br /> PrintForm1.PrintAction = Printing.PrintAction.PrintToPreview ' Crea una vista previa<br /> ' PrintForm1.PrintAction = Printing.PrintAction.PrintToFile ' Imprime a un archivo<br /> ' PrintForm1.PrintAction = Printing.PrintAction.PrintToPrinter ' Envía a la impresora<br /> PrintForm1.Print() ' Finalmente hacemos la impresión<br /> End Sub</pre>Pablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.com2tag:blogger.com,1999:blog-3398533864216934520.post-87029376533738452902009-02-27T04:49:00.000-08:002009-02-27T04:50:26.429-08:00Utilizando el portapapeles de Windows en nuestras aplicaciones .NETPara manejar el <strong>portapapeles de Windows</strong>, el .Net Framework provee la clase <strong>Clipboard</strong>, del namespace <strong>My.Computer</strong>la cual posee todo lo necesario para copiar y pegar archivos, textos, imágenes, etc.<br /><br /><br />Veamos como copiar elementos al portapapeles:<br /><br /><ul><br /> <li><code>My.Computer.Clipboard.SetImage(PictureBox1.Image)</code> - Copia la imagen del picturebox</li><br /> <li><code>My.Computer.Clipboard.SetText(WebBrowser1.DocumentText, TextDataFormat.Html)</code> - Copia el código HTML del documento que estamos mostrando en el WebBrowser como texto y setea el formato de éste como HTML.</li><br /> <li><code>My.Computer.Clipboard.SetText(RichTextBox1.Rtf, TextDataFormat.Rtf)</code> - Ídem anterior pero formato RTF.</li><br /> <li><code>My.Computer.Clipboard.SetText(TextBox1.Text)</code> - Copia un texto cualquiera, sin formato.</li><br /> <li><code>My.Computer.Clipboard.SetData(MyClassInstance.GetType().Name, MyClassInstance)</code> - Copia el objeto MyClassInstance al portapapeles.</li><br /></ul><br /><br />Ahora tenemos que recuperar los objetos que copiamos al portapapeles:<br /><br /><ul><br /> <li><code>PictureBox2.Image = My.Computer.Clipboard.GetImage()</code> - Recupera la imagen copiada al portapapeles y la setea como Image en el PictureBox2.</li><br /> <li><code>WebBrowser2.DocumentText = My.Computer.Clipboard.GetText(TextDataFormat.Html)</code> - Obtiene el texto con formato HTML</li><br /> <li><code>RichTextBox2.Rtf = My.Computer.Clipboard.GetText(TextDataFormat.Rtf)</code> - Idem anterior con formato RTF.</li><br /> <li><code>TextBox2.Text = My.Computer.Clipboard.GetText()</code> - Recupera un texto sin formato y lo asigna en TextBox2.Text</li><br /> <li><code>Dim MyClassNewInstance As MyClass = CType(My.Computer.Clipboard.GetData(GetType(MyClass).Name), MyClass)</code> - Recupera el objeto de tipo MyClass que copiamos anteriormente.</li><br /></ul><br /><br />Para limpiar ó vaciar el portapapeles utilizaremos:<br /><br /><ul><br /> <li><code>My.Computer.Clipboard.Clear()</code></li><br /></ul><br /><br />Algunos controles que podemos hacer sobre la info contenida en el portapapeles:<br /><br /><ul><br /> <li><code>If My.Computer.Clipboard.ContainsImage() Then</code> - Para saber si hay una imagen en el portapapeles</li><br /> <li><code>If My.Computer.Clipboard.ContainsText(TextDataFormat.Html)</code> - Para saber si el portapapeles contiene un texto en formato HTML</li><br /> <li><code>If My.Computer.Clipboard.ContainsData(GetType(MyClass).Name) Then</code> - Comprueba si hay un objeto del tipo MyClass en el portapapeles.</li><br /></ul><br /><br />Al preguntar por <code>My.Computer.Clipboard.ContainsText()</code>, se puede preguntar por varios formatos, los mismos se especifican en el <strong>Enum TextDataFormat</strong>:<br /><br /><ul><br /> <li>CommaSeparatedValue</li><br /> <li>Html</li><br /> <li>Rtf</li><br /> <li>Text</li><br /> <li>UnicodeText</li><br /></ul><br /><br />Y ese sería un resumen de los usos más frecuentes del portapapeles, aunque también se puede utilizar de varias maneras más, estas son las que consideré más útiles.<br /><br />Una aclaración con respecto a copiar y pegar objetos, yo utilicé como nombre del formato el nombre del tipo tal cual se devuelve por <code>GetType().Name</code>, en ese parámetro se puede escribir cualquier String, pero considero una <strong>buena práctica</strong> utilizar el nombre del tipo para que al recuperar sepamos qué es lo que estamos buscando.Pablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.com0tag:blogger.com,1999:blog-3398533864216934520.post-59734839535820350282009-02-11T07:27:00.000-08:002016-06-02T20:56:17.529-07:00Trabajando con Archivos y Carpetas: System.IO.PathLa clase <b>System.IO.Path</b> provee una serie de métodos para realizar las tareas más comunes a la hora de trabajar con rutas de archivos o directorios, a continuación veremos los métodos de esta clase y para qué sirve cada uno.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhvThcWNMJsXdj9yAjJQ1VXEoecUSiEQ6wcIONlrQOqjQzUgppxc-xcvxmlc3FMUBganIkiFAsnAD59lEIbMfkQ7eoy0zAX-4D-kzrHvPHykJsZaR7k56wqO2KUL-XJhnmlFkevO1tHpAk/s1600/System.IO.Path.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="244" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhvThcWNMJsXdj9yAjJQ1VXEoecUSiEQ6wcIONlrQOqjQzUgppxc-xcvxmlc3FMUBganIkiFAsnAD59lEIbMfkQ7eoy0zAX-4D-kzrHvPHykJsZaR7k56wqO2KUL-XJhnmlFkevO1tHpAk/s320/System.IO.Path.png" width="320" /></a></div>
<br />
<ul><br />
<li><code>System.IO.Path.AltDirectorySeparatorChar</code> : Devuelve el caracter alternativo usado como separador de directorios en el sistema actual. Generalmente "/".</li>
<br />
<li><code>System.IO.Path.ChangeExtension("C:\prueba.txt", "xml")</code> : Cambia la extensión del archivo pasado como primer parámetro, en este ejemplo devuelve C:\prueba.xml.</li>
<br />
<li><code>System.IO.Path.Combine("C:\Directorio1\", "prueba.xml")</code> : Combina ambas rutas para generar una sola. Atención al segundo parámetro porque si empieza con el separador de directorio no funcionará como esperamos.</li>
<br />
<li><code> System.IO.Path.DirectorySeparatorChar</code> : Devuelve el caracter usado como separador de directorios en el sistema actual. Generalmente "\".</li>
<br />
<li><code>System.IO.Path.GetDirectoryName("C:\Directorio1\prueba.txt")</code> : Devuelve la ruta al directorio según la ruta que recibe por parámetro. En este ejemplo devuelve "C:\Directorio1"</li>
<br />
<li><code>System.IO.Path.GetExtension("C:\Directorio1\prueba.xml")</code> : Devuelve la extensión del archivo que recibe por parámetro, por el ejemplo ".xml" si el archivo no tiene extensión devuelve "".</li>
<br />
<li><code>System.IO.Path.GetFileName("C:\Directorio1\prueba.xml")</code> : Devuelve el nombre del archivo especificado en el parámetro sin el resto de la ruta. En este ejemplo devuelve "prueba.xml". Si la ruta especificada termina con el separador de directorios, devuelve "".</li>
<br />
<li><code>System.IO.Path.GetFileNameWithoutExtension("C:\Directorio1\prueba.xml")</code> : Idem que <code>GetFileName()</code> pero sin extensión. En el ejemplo "prueba"</li>
<br />
<li><code>System.IO.Path.GetFullPath("C:")</code> Devuelve la ruta actual sobre la unidad C: Por ejemplo "C:\miApp\bin\Debug</li>
<br />
<li><code>System.IO.Path.GetFullPath("\prueba1")</code> : Devuelve la ruta absoluta, toma en cuenta la unidad actual o la ruta actual.</li>
<br />
<li><code>System.IO.Path.GetInvalidFileNameChars()</code> : Devuelve los caracteres que no pueden formar parte del nombre de un archivo</li>
<li></li>
<br />
<li><code>System.IO.Path.GetInvalidPathChars()</code> : Devuleve los caracteres no válidos para una ruta.</li>
<br />
<li><code>System.IO.Path.GetPathRoot("C:\Directorio1\prueba.xml")</code> : Devuelve la raíz de la ruta especificada, en este caso "C:\"</li>
<br />
<li><code>System.IO.Path.GetRandomFileName()</code> : Devuelve un nombre randómico para usar como nombre de archivo o directorio.</li>
<br />
<li><code>System.IO.Path.GetTempFileName()</code> : Devuelve un nombre de archivo temporal, apuntando al directorio especificado en %TEMP%. Por ejemplo "C:\Documents and settings\usuario\Configuración local\Temp\tmp131E.tmp"</li>
<br />
<li><code>System.IO.Path.GetTempPath()</code> : Devuelve la ruta al directorio temporal, siguiendo con el ejemplo anterior, "C:\Documents and settings\usuario\Configuración local\Temp\"</li>
<br />
<li><code>System.IO.Path.HasExtension("C:\prueba.xml")</code> : Devuelve un valor Boolean indicando si la ruta especificada contiene una extensión, en el ejemplo la extensión es ".xml" o sea que devuelve <code>True</code></li>
<br />
<li><code>System.IO.Path.IsPathRooted("..\Directorio1\prueba.xml")</code> : Devuelve un valor Boolean indicando si la ruta especificada es absoluta o no, en el ejemplo la ruta es relativa por lo que devuelve <code>False</code></li>
<br />
<li><code>System.IO.Path.PathSeparator</code> : Devuelve el separador de rutas, generalmente ";"</li>
<br />
<li><code>System.IO.Path.VolumeSeparatorChar</code> : Devuelve el caracter separador de volúmenes, generalmente ":"</li>
</ul>
<br />
<br />
Estos son todos los miembors de la clase <b>System.IO.Path</b> con una breve descripción de cada uno.Pablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.com4tag:blogger.com,1999:blog-3398533864216934520.post-28790546959984904442009-02-06T04:38:00.000-08:002009-03-12T23:19:39.752-07:00Problema al crear un WebService: Failed to access IIS metabase - SOLUCIONADOIntentando crear un <strong>WebService con C#</strong> me encontré con que cada vez que intentaba ejecutarlo <abbr title="Internet Information Server">IIS</abbr> me devolvía el siguiente error: <strong>Failed to access IIS metabase problem</strong>.<br /><br />Busqué por acá y por alla, descargué un código <abbr title="Visual Basic Script">VBS</abbr> pero nada... después de probar las mil y una soluciones encontré <strong>la solución definitiva</strong>, así que decidí publicarla aquí para tenerla siempre a mano y de paso le puede servir a alguien más.<br /><br />La <strong>solución</strong> consiste en abrir una consola, ir al directorio de instalación del Framework, 2.0 en este caso y ejecutar un comando que hará una reinstalación de <strong>ASP.Net</strong><br /><br /><div style="background-color: #000; color: #fff;"><br /><code><pre><br />C:\> cd WINDOWS\Microsoft.NET\Framework\v2.0.50727<br /><br />C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\> aspnet_regiis.exe -i<br /><br />Start installing ASP.NET (2.0.50727).<br />......................<br />Finished installing ASP.NET (2.0.50727).<br /><br />C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\><br /><br /><br /></pre></code><br /></div>Pablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.com1tag:blogger.com,1999:blog-3398533864216934520.post-36435886951109007852009-01-16T04:18:00.001-08:002009-01-16T12:37:52.960-08:00MethodInvoker: un delegado simple integrado en el frameworkMuchas veces necesitamos hacer un <strong>Invoke</strong>, para las veces que necesitamos invocar un método simple, sin parámetros podemos usar el delegado <strong>MethodInvoker</strong>, que no es más que un <strong>Delegate sub</strong> sin parámetros:<br /><br /><code>Public Delegate Sub MethodInvoker()</code><br /><br />O sea que si tenemos que utilizarlo podríamos por ejemplo ahcer lo siguiente:<br /><br /><code>Dim d as MethodInvoker = New MethodInvoker(AddressOf MyProcedimiento)</code><br /><br /><br />Simplemente un tip, que me fue útil, ya que hasta que lo encontré declaraba mi propio <strong>delegate</strong>.Pablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.com0tag:blogger.com,1999:blog-3398533864216934520.post-80527982781581891802009-01-14T05:25:00.000-08:002009-01-14T06:23:29.486-08:00101 Ejemplos de LINQ en VB.NetUn muy buen recurso para aprender a dominar esta gran tecnología, tiene ejemplos de LINQ para SQL, Dataset, XML y Objetos, todos con código fácil de comprender.<br /><br />Sin duda es un enlace que vale la pena tener a mano.<br /><br /><br /><br />Enlace: <a href="http://msdn.microsoft.com/es-es/vbasic/bb688088(en-us).aspx" target="_blank" title="MSDN: 101 ejemplos de LINQ">http://msdn.microsoft.com/es-es/vbasic/bb688088(en-us).aspx</a>Pablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.com0tag:blogger.com,1999:blog-3398533864216934520.post-38836918991068748812009-01-05T16:14:00.000-08:002009-01-05T16:28:13.216-08:00Recursividad + Invoke = Procedimiento Thread SafeEste es más un apunte que un gran conocimiento, pero puede ayudar a más de uno, la idea es mostrar datos en la <abbr title="User Interface">UI</abbr> desde un hilo diferente del principal, por ejemplo: creamos un hilo que se encargue de ir a buscar datos a la <abbr title="Base de Datos">BDD</abbr> y los muestre en una grilla cuando los reciba.<br /><br />Cuando aprendí a usart <strong>Invoke</strong> lo que hacía era un método que mostraba los datos en la <abbr title="User Interface">UI</abbr> y otro que chequeaba si era necesario utilizar <strong>Invoke</strong> o no.<br /><br /><pre><code>Public Sub MostrarDatos(Datos as DataSet)<br /> If Me.InvokeRequired = True Then<br /> Me.Invoke(New SetDataSource_Delegate(AddressOf SetDataSource), Datos)<br /> Else<br /> Me.SetDataSource(Datos)<br /> End If<br />End Sub<br /><br /><br />Private Delegate Sub SetDataSource_Delegate(ds As DataSet)<br /><br />Private Sub SetDataSource(ds As DataSet)<br /> Me.DatagridView1.DataSource = ds<br /> Me.DatagridView1.DataMember = ds.Tables(0).TableName<br />End Sub</code></pre><br /><br />Un tiempo después, como parte de mi eterna búsqueda por escribir mejor código, descubrí que podía simplificar todo eso usando una llamada recursiva y combinando ambos procedimientos en uno solo de la siguiente manera:<br /><br /><pre><code>Private Delegate Sub MostrarDatos_Delegate(ds As DataSet)<br /><br /><br />Public Sub MostrarDatos(Datos as DataSet)<br /> If Me.InvokeRequired = True Then<br /> Me.Invoke(New MostrarDatos_Delegate(AddressOf MostrarDatos), Datos)<br /> Else<br /> Me.DatagridView1.DataSource = ds<br /> Me.DatagridView1.DataMember = ds.Tables(0).TableName<br /> End If<br />End Sub</code></pre><br /><br />Como se puede ver, el código queda más legible y no deja de ser eficiente, además de ahorrarnos un par de líneas... que en un formulario muy extenso puede ayudarnos bastante.Pablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.com3tag:blogger.com,1999:blog-3398533864216934520.post-88782361102101179362008-12-23T14:57:00.000-08:002008-12-23T15:16:25.762-08:00Tipos anónimos en .NetLos <strong>tipos anónimos</strong> son una nueva característica de .Net que utilizada junto a <a href="http://aprendiendonet.blogspot.com/search/label/LINQ">LINQ</a> nos brinda grandes ventajas a la hora de escribir nuestro código.<br /><br />Primero que nada, deberemos chequear que la opción <strong>Option Infer</strong> esté habilitada en nuestro proyecto. Para ello iremos a Propiedades del proyecto, en la pestaña Compilación. Por supuesto, al principio podremos tener algunos problemas si también utilizamos la opción <strong>Option Srtict</strong>, pero es cuestión de costumbre.<br /><br />Volviendo a los tipos anónimos, al escribir una sentencia en <strong>LINQ</strong>, podremos definirnos una variable sin tipo y a pesar de ello tener intellisense.<br /><br />Por ejemplo:<br /><br /><code><pre>//C#<br />var hola = from x in myDdatacontext.table1<br /> where x.id > 20<br /> select x;<br /><br /><br />'VB.Net<br /><br />Dim Hola = From x in MyDataContext.Table1 _<br /> Where x.Id > 20 _<br /> Select x<br /></pre></code><br /><br />En realidad nuestras variables Hola, tomarán, en este caso el tipo <strong>IQueriable(Of Table1)</strong> con lo cual podremos ir navegando a través de los items devueltos por la consulta.<br /><br />Suponiendo que Table1 tiene los campos Id, Nombre y Apellido, al escribir <code>x.</code> el intellisense nos mostrará como proiedades Id, Nombre y Apellido. Y de igual manera, cuando recorremos nuestras variables de tipos anónimos podremos ver el intellisense y acceder a cada propiedad, por ejemplo:<br /><br /><code><pre>'VB.Net<br />For Each x in Hola<br /> Console.Writeline(String.Format("ID: {0} Nombre: {1} Apellido: {2}", x.Id, x.Nombre, x.Apellido))<br />Next<br /></pre></code><br /><br />Otra forma, sería si quisiéramos devolver el primer elemento del resultado de la consulta, entonces:<br /><br /><code><pre>' VB.Net<br />Return Hola.First<br /></pre></code>Pablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.com0tag:blogger.com,1999:blog-3398533864216934520.post-91706478193934389482008-11-22T08:39:00.000-08:002008-11-22T08:47:43.601-08:00Microsoft Beginner Developer Learning CenterMicrosoft ha publicado una serie de tutoriales y videos orientados a los nuevos programadores, la idea es enseñarles programación utilizando la serie Express de herramientas para programar .Net.<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://msdn.microsoft.com/es-ar/beginner/default.aspx" title="Microsoft Beginner Developer Learning Center"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 320px; height: 97px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh2Qcc512uvEBxos3uskZl_f6QiMnfzJYB0huJJpFkcjGsdGkty98HKYJAOuTJvCN80ghCTUlAzJpU_ED6OA_Vpkq20RYjdgM4mOwd76jroqxYpLgRFTKofFgahu5Rux3h8f7ezQ9SiaBM/s320/microsoft-learning-center.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5271524294023818450" /></a>Pablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.com0tag:blogger.com,1999:blog-3398533864216934520.post-15940133588037864162008-09-11T07:13:00.000-07:002008-09-11T07:46:54.345-07:00Nueva Academia Virtual de Microsoft<a href="http://www.mslatam.com/latam/technet/mva/" title="Click para visitar la Microsoft Virtual Academy"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjMV1WmfVqph8kTFdLTyoZYFhHdZncLlrMkcuNluGsiDj6CN-Md8Ogk3_je68Mp2mLtILAzQoGiqnF7miq5MwxvbsouqEhbUi4XyNUEtAJrXApF58iJrCRMW8DbMmUyx4NXRHWHMEJExl8/s320/microsoft-virtual-academy.jpg" border="0" alt="Microsoft Virtual Academy - mva"id="BLOGGER_PHOTO_ID_5244768192326969778" /></a><br /><br />Microsoft ha lanzado una nueva <strong>Academia Virtual</strong> llamada <strong>Microsoft Virtual Academy</strong>. En la misma presentan cursos para aprender y para actualizarse seleccionando una carrera, igual que una academia real.<br /><br /><strong>MVA</strong> es una forma gratuita de capacitarse, puediendo anotarte en varios cursos y carreras, en los cuales irás obteniendo puntos según tu desempeño con los cuales podrás solicitar una membresía, obtener descuentos y varios beneficios más.<br /><br />Enlace: <a title="Microsoft Virtual Academy" href="http://www.mslatam.com/latam/technet/mva/">mva</a>Pablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.com0tag:blogger.com,1999:blog-3398533864216934520.post-48933144231958468882008-08-28T11:50:00.000-07:002008-08-28T12:12:57.065-07:00Como mostrar un dialogo de confirmación antes de cerrar la ventanaEs lo más común, se va a cerrar una ventana, pero se han editado datos y no se han guardado. En ese caso es muy conveniente utilizar un diálogo con los botones Si, No, Cancelar, los cuales utilizaremos para Guardar los cambios, Descartarlos o cancelar el cerrado de la ventana respectivamente.<br /><br />Lo primero que necesitamos es alguna rutina o bandera que nos indique si se han realizado cambios en nuestra ventana, para este ejemplo será una variable de tipo Boolean que se seteará en True cada vez que se modifique un dato.<br /><br />Luego escribiremos una rutina en el manejador del evento <strong>FormClosing</strong> de nuestro Form que mostrará el diálogo y dependiendo de la respuesta del usuario realizará una acción:<br /><br /><code><pre> Private Sub Form1_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing<br /> If Me.HayCambios Then<br /> Dim respuesta As DialogResult = MessageBox.Show("Hay cambios pendientes, desea guardarlos antes de salir?", "Cambios pendientes", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2)<br /> Select Case respuesta<br /> Case Windows.Forms.DialogResult.Yes<br /> Me.GuardarArchivoComo()<br /> Case Windows.Forms.DialogResult.No<br /> 'Descartar los cambios <br /> Case Windows.Forms.DialogResult.Cancel<br /> e.Cancel = True<br /> End Select<br /> End If<br /> End Sub</pre></code><br /><br /><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh7zhF3F0DGmVcDWf1HQjH-F3ZyUHfyLeme8Wz5WugLbdkJVBuzm0dY8cj0u_pYcSu2gk8MPfS7kXvQjI1KzSu3jKdEpchpOvHGg2Ft_NKnavyrbewd5kHv8XUeW-TJ0AfIDDthqqqDLNE/s1600-h/dialogo-guardar-cambios-pendientes.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh7zhF3F0DGmVcDWf1HQjH-F3ZyUHfyLeme8Wz5WugLbdkJVBuzm0dY8cj0u_pYcSu2gk8MPfS7kXvQjI1KzSu3jKdEpchpOvHGg2Ft_NKnavyrbewd5kHv8XUeW-TJ0AfIDDthqqqDLNE/s320/dialogo-guardar-cambios-pendientes.png" border="0" alt="Mostrar un diálogo de confirmación antes de cerrar la ventana"id="BLOGGER_PHOTO_ID_5239648437907599090" /></a>Pablo Rhttp://www.blogger.com/profile/11356887562766169507noreply@blogger.com0