sábado, 19 de abril de 2008

Extendiendo controles con GDI+

Que en los tiempos de Windows Presentation Foundation y Silverlight alguien dedique su tiempo a hablar de Windows Forms y GDI+ puede parecer, cuanto menos, curioso. Pero seguro que para algunos de aquellos que todavía no han dado el salto al Framework 3 (o 3.5) este post les puede ser de utilidad.

El objetivo del post es mostrar como podemos mejorar el aspecto grafico de nuestras aplicaciones de manera muy sencilla con GDI. Concretamente en el ejemplo se verá como modificar un control TextBox para poder modificar, mediante GDI el color del borde del mismo (una propiedad que el control no tiene).

Lo primero que hay que hacer es crear una clase y hacer que herede de TextBox, por ejemplo así:

class ExtendedTextBox : System.Windows.Forms.TextBox

En este momento tenemos una clase que tiene exactamente las mismas funcionalidades que un TextBox. Nuestro objetivo, cómo hemos dicho, es poder modificar el color del borde, para ello y antes de entrar realmente en el tema de GDI tenemos que preparar nuestra clase para permitir al que se pueda modificar el borde. Para ello podemos hacer lo siguiente:

  1. Declaramos una variable privada que nos servirá para guardar el color del borde actual:-> private Color m_cBorderColor;
  2. En el constructor hacemos lo siguiente:
    1. Inicializamos la variable anterior al color por defecto: m_cBorderColor = Color.Black;
    2. Definimos una propiedad que nos permita cambiar el color del borde, por ejemplo :

public Color BorderColor
{

get{return m_cBorderColor;

}

set{m_cBorderColor = value;}

}

Ahora ya tenemos preparada la clase para que nos puedan modificar el color del borde, pero aunque lo hagan, esto no tendrá ningún efecto real sobre nuestro control (únicamente estamos modificando el valor de la variable m_cBorderColor). Para que este color se aplique a nuestro TextBox, tenemos que usar GDI+. ¿Y cómo se hace esto? Pues lo primero que tenemos que hacer es sobrescribir el evento OnPaint de nuestra clase:


protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
{}

Y lo segundo, es especificar en este método que es lo que queremos pintar (en nuestro caso es el color del borde del control, pero se podría modificar, dibujar, o pintar todo lo que se quisiese). El código del método quedaría así:

protected override void OnPaint(System.Windows.Forms.PaintEventArgs e){
Graphics g = e.Graphics;
Pen p = new Pen(m_cBorderColor, 1);
g.DrawRectangle(p, new Rectangle(0, 0, this.Width - 1, this.Height - 1));
base.OnPaint(e);
}


Básicamente lo que estamos haciendo es acceder a la superficie de pintado del control (clase Graphics) , definir un Pincel (Pen) con el color almacenado en nuestra variable y un píxel de grosro, y dibujar un rectángulo que coincida con los bordes del textbox. Después simplemente se llama a la clase base para acabar de hacer el renderizado.

Con esto, parece que ya tendríamos suficiente para poder utilizar nuestro control en un formulario, pero si lo intentáis utilizar veréis que al modificar el color del control este no cambia. Esto pasa porque no le estamos diciendo al control que se repinte al modificar el color, para ello tenemos que modificar la propiedad BorderColor de la siguiente manera:
set
{
m_cBorderColor = value;
this.OnPaint(new PaintEventArgs(this.CreateGraphics(), this.ClientRectangle)); <- Repintar

}

Ahora, si modificáis el color, veréis como el borde se cambia, ¡pero aun no es suficiente! Si después de modificar el color , el formulario se minimiza o alguna otra ventana se pode delante, veréis cómo el control vuelve a perder el color que le hemos dado. ¿Por qué sucede esto? Porque las llamadas que el sistema operativo hace a la función de repintar del TextBox, ignoran por defecto a las funciones que sobrescriben a la función base (es decir no se está ejecutando la función
protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) cuando el sistema operativo lo requiere. Una opción seria implementar en el control todos los posibles casos en que se puede dar esta casuística y llamar nosotros mismos a la función (al igual que hacemos en el set de la propiedad) pero esta solución, aparte de costosa es errónea. La solución pasa por usar la siguiente instrucción en el constructor de la clase:

SetStyle(ControlStyles.UserPaint, true);

Esta instrucción modifica el comportamiento por defecto y hace que nuestra función se ejecute cuando el sistema operativo necesite repintar el control.

Por último, debemos implementar el evento TextChanged del control, para que cada vez que se escriba una letra en el mismo se repinte el control.

this.TextChanged += new EventHandler(ExtendedTextBox_TextChanged); <- en el constructor //Implementación del método que captura el evento void ExtendedTextBox_TextChanged(object sender, EventArgs e) { this.OnPaint(new PaintEventArgs(this.CreateGraphics(), this.ClientRectangle)); } A partir de este momento ya tenemos un nuevo control TextBox al que fácilmente le podemos modificar el color del borde. De la misma manera que hemos visto este ejemplo, con GDI+ se pueden hacer cosas mucho más complejas, permitiendo tener aplicaciones Windows mucho más ricas y personalizadas, y con un alto valor añadido.

Podeis descargar el código del ejemplo aquí.


lunes, 7 de abril de 2008

Excepciones en entorno asíncrono

Para mi primer post "serio", he decidido comentar brevemente el tema de las excepciones cuando estamos trabajando en entornos asíncronos. Y he escogido el tema porque hace muy poco tiempo, en la oficina un compañero me pidió ayuda cuando se estaba peleando con un tema muy similar.

Para los que no sepan muy bien que es trabajar con operaciones asíncronas, se podría decir que, resumiendo mucho, se trata de hacer que operaciones que suelen llevar mucho tiempo se ejecuten en segundo plano y permitan que la aplicación o el sistema puedan llevar a cabo otras tareas (típicamente mostrar información de progreso al usuario o permitirle realizar otras acciones en paralelo, etc.).En el siguiente enlace de la msdn se explica todo lo referente a la programación asíncrona :
Asynchronous Programming Design Patterns

El objetivo del post, es mostrar cómo se pueden capturar las excepciones en entornos asíncronos. La teoría es bastante sencilla, y basta con decir que en entornos asíncronos las excepciones se capturan siempre en el End o en el evento de retorno, pero a la hora de la verdad la cosa se complica un poco más. Para verlo más claro ilustraré cómo capturar una excepción en un típico caso de instrucción asíncrona (una llamada a un WebService) con un sencillo ejemplo:

Hemos definido un servicio Web que está disponible en una dirección cualquiera, y instanciamos la clase en nuestro código de la siguiente manera:

AsyncExampleWebService.AsyncExceptionWebService ws = new DevNetTips.AsyncExceptions.AsyncExampleWebService.AsyncExceptionWebService();

El servicio web tiene un método llamado ExceptionTest, al cual se le pasa un entero cómo parámetro. Si el parámetro és un 0 lanza una excepción.

Podríamos llamar al método de la siguiente manera:

ws.ExceptionTest(0); //llamada SÍNCRONA al método

Pero supongamos que es un método que puede tardar bastante en ejecutarse, y lo que queremos es que mientras estamos esperando el resultado, el usuario pueda seguir haciendo cosas. Para ello hay que usar el método:

ws.ExceptionTestAsync(param); //lamada Asíncrona al método.

Esto hará que la aplicación no se quede bloqueada hasta que el resultado sea devuelto, pero... como sabemos cuando ha acabado el método? En este caso (después veremos que hay maneras diferentes de llamar a operaciones asíncronas) se usa el evento definido en el servicio Web (hay que recordar que esto es automático, en el servicio únicamente se define el método ExceptionTest, ni el ExceptionTestAsync ni el evento).

El código quedaría así:

ws.ExceptionTestCompleted += new DevNetTips.AsyncExceptions.AsyncExampleWebService.ExceptionTestCompletedEventHandler(ws_ExceptionTestCompleted);
ws.ExceptionTestAsync(param);

void ws_ExceptionTestCompleted(object sender, DevNetTips.AsyncExceptions.AsyncExampleWebService.ExceptionTestCompletedEventArgs e)
{

}

El método ws_ExceptionTestCompleted se llama justo en el momento en el que el resultado del método ExceptionTest es devuelto. Este método tiene un evento de tipo ExceptionTestCompletedEventArgs que contiene toda la información relativa a la ejecución del método. Consultando la propiedad Result podemos saber lo que ha devuelto el método. ¿Pero que pasa si se produce una excepción?

Lo más instintivo supongo que sería intentar capturar la excepción al hacer la llamada al método ws.ExceptionTestAsync(param)

try{
ws.ExceptionTestAsync(param);
}
catch(Exception e)
{
//código de tratamiento de la excepción
}

Pero esto NO funciona. ¿Por qué? Porque estamos realizando una llamada a una operación asíncrona, y cómo hemos comentado un poco antes, las excepciones se capturan en el evento de retorno de la función.

Vemos que este caso concreto es muy sencillo, ya que únicamente se debe consultar el valor e.Error del parámetro ExceptionTestCompletedEventArgs del método de retorno. Si esta propiedad no es nula significa que se ha producido una excepción en la llamada al método. De una manera muy elegante se consigue capturar una excepción en una operación asíncrona. El código del método que captura el evento quedaría al final así:

void ws_ExceptionTestCompleted(object sender, DevNetTips.AsyncExceptions.AsyncExampleWebService.ExceptionTestCompletedEventArgs e)
{
if (e.Error == null)
{
Console.WriteLine(e.Result);
}
else
{
Console.WriteLine(e.Error.Message);
}
Console.WriteLine("");
}

Esta sencilla manera de hacer llamadas a operaciones asíncronas y capturar las posibles excepciones no es el único mecanismo que proporciona .Net. En lugar del modelo basado en eventos que hemos visto, existe el modelo basado en objetos IAsyncResult. Éste modelo (que a su vez se puede utilizar de varias maneras diferentes) permite muchas mas flexibilidad que el primero, aunque aumenta un poco su complejidad. Un sencillo ejemplo que muestra cómo hacer una llamada a un hostname ilustra una de los posibles maneras de hacer una llamada asíncrona y cómo capturar una posible excepción:

//llamada al método BeginGEtHostEntry. Inicio de la operación asíncrona. Se define un callback que se ejecutará cuando la ejecución del método hay finalizado
IAsyncResult result = Dns.BeginGetHostEntry("172.19.110.12", new AsyncCallback(DNSCallback), null);

//función de callback
private void DNSCallback(IAsyncResult result)
{
try
{
// se llama al método EndGetHostEntry. Final de la operación asíncrona.
//Aquí hay que capturar la posible excepción. La función devuelve el mismo tipo que la función síncrona
IPHostEntry host = Dns.EndGetHostEntry(result);
string[] aliases = host.Aliases;
IPAddress[] addresses = host.AddressList;
if (aliases.Length > 0)
{
Console.WriteLine("Aliases");
for (int i = 0; i <> 0)
{
Console.WriteLine("Addresses");
for (int i = 0; i <>
{
Console.WriteLine("{0}",addresses[i].ToString());
}
}
}
catch (SocketException e)
{
Console.WriteLine("An exception occurred while processing the request: {0}", e.Message);
}
Console.WriteLine("");
}


Para profundizar más en el uso de operaciones asíncronas y en concreto con IAsyncResult podeis visitar el siguiente link :
Calling Asynchronous Methods Using IAsyncResult


Descargar Código de los ejemplos