Mi experiencia con Silverlight no está siendo todo lo placentera que me gustaría. Me estoy encontrado permanentemente en la necesidad de cambiar las cosas que presume por defecto esta tecnología, para adecuarla a mis necesidades.
Una de ellas ha sido la creación de un control Textbox con marca de agua. Buscando un poco por Internet el primer enlace redirige al blog de Tim Heuer, donde tenemos un ejemplo de implementación de este control.
El problema se plantea cuando se quiere utilizar este control para usarlo como control de password. No nos ofrece la posibilidad de enmascarar los caracteres. La solución podría ser algo como esto (no recuerdo si se me ocurrió a mí, lo leí por ahí o adapté algo que hubiera encontrado):
/// <summary>
/// Changes the visual state when the control changes its text.
/// </summary>
/// <param name="sender">Sender of the event.</param>
/// <param name="e">Event arguments.</param>>
private void OnTextChanged(object sender, TextChangedEventArgs e)
{
if (this.IsPasswordBox == true)
{
TextBox textBox = sender as TextBox;
if (textBox != null)
{
string newText = textBox.Text;
// If we don't have text, clean the plain text back up.
if (string.IsNullOrEmpty(newText) == true)
{
this.Plaintext = string.Empty;
return;
}
// If the new text is shorter than the previous text, we simply cut the previous text
if (newText.Length < this.Plaintext.Length)
{
this.Plaintext = this.Plaintext.Substring(0, newText.Length);
return;
}
// We have an string longer than the previous one. We can have a new character in any place
// inside the text, so we go along the string to find the new characters (probably only one).
// This new character will not be the "hidden" character
StringBuilder newBackup = new StringBuilder();
// We will use the offset because every time we find a character in the new text that is clear,
// it is a new character. For example, if the new character is in the 5º position, and the next
// 6º character is hidden, it would be in the 5º position of the old text. So we use the offset
// to count in which place the character is in the old text.
int offset = 0;
for (int i = 0; i < newText.Length; i++)
{
if (newText[i] == 'u25CF')
{
// We add this character from the original plain text to the new one
newBackup.Append(this.Plaintext[i + offset]);
}
else
{
// We add this character from the new text, because it's one of the new characters
newBackup.Append(newText[i]);
// We must update the offset
offset--;
// We must change this character in the new text for the hidden character.
newText = newText.Substring(0, i) + 'u25CF' + newText.Substring(i + 1, newText.Length - i - 1);
}
}
// Set the masked text in the control
textBox.Text = newText;
// Move the caret to the end of the text
textBox.SelectionStart = textBox.Text.Length;
// Update the backup plain text
this.Plaintext = newBackup.ToString();
}
}
this.ChangeVisualState(true);
}
Por último, quedaría la validación del control. Por desgracia para los que no estamos muy informados sobre cómo funciona Silverlight, el ejemplo de Tim no incluía nada relacionado con la validación del control. Es decir, la validación en sí se realiza con mecanismos ajenos al control, como los Data Annotations. El problema es que, una vez validado el control y encontrado que no es correcta su información, es necesario que pase a un estado “No Válido” y que muestre alguna forma de error.
Después de darle un poco de vueltas y echarle un vistazo a estilos de otros controles por defecto de Silverlight, ésta es la solución que me está funcionando a las mil maravillas (esta vez, sí, cosecha propia 100%).
En primer lugar, el estilo para el control. Me voy a limitar a mostrar las partes directamente relacionadas con la validación, puesto que al final del artículo adjunto tanto el estilo del control como el código del mismo.
A destacar especialmente que hay dos partes claramente diferenciadas en el estilo:
La parte en la que se definen el grupo de estados “ValidationStates”, formado por los estados “Valid”, “InvalidUnfocused” y “InvalidFocused”. Con estos tres cubriríamos todas las posibilidades relacionadas con el estado del control y su validación.
La otra parte relevante del estilo es la definición de un elemento “ValidationErrorElement”, que junto al tooltip nos permite marcar, en este caso en rojo, el control que ha fallado y mostrar un mensaje con información del error.
La segunda parte de este truco consiste en definir las transacciones correspondientes entre los estados del control así como dispararlas ante eventos concretos que acontezcan en el control. En este caso, es necesario definir una propiedad “isErroneous” para indicar cuándo el control ha pasado al estado erróneo. Esta propiedad la controlaremos conectándonos al evento BindingValidationError, tal como se muestra en el siguiente extracto del código del control:
/// <summary>
/// Indicates if the control is in an erroneous state.
/// </summary>
private bool isErroneous;
/// <summary>
/// Initializes a new instance of the <see cref="WatermarkedTextBox"/> class.
/// </summary>
public WatermarkedTextBox()
{
this.BindingValidationError += new EventHandler<ValidationErrorEventArgs>(WatermarkedTextBox_BindingValidationError);
}
El código que vamos a ligar al evento es tan sencillo como lo que podemos ver a continuación:
/// <summary>
/// Configures the properties of the control to indicate it has an error.
/// </summary>
/// <param name="sender">Sender of the event.</param>
/// <param name="e">Event arguments.</param>
private void WatermarkedTextBox_BindingValidationError(object sender, ValidationErrorEventArgs e)
{
this.isErroneous = e.Action == ValidationErrorEventAction.Added ? true : false;
this.ChangeVisualState(true);
}
Controlando la acción que acompaña como argumento al evento, podremos saber si la función ha saltado porque el control ha entrado o ha salido del estado erróneo. Toda la lógica, y por tanto lo relevante, queda relegado al método ChangeVisualState. Este método controlará las transacciones entre los distintos estados en los que puede estar el control. Un extracto de este método, específico para la parte de validación de errores, en el siguiente fragmento:
/// <summary>
/// Change to the correct visual state for the textbox.
/// </summary>
/// <param name="useTransitions">
/// true to use transitions when updating the visual state, false to
/// snap directly to the new visual state.
/// </param>
private void ChangeVisualState(bool useTransitions)
{
// Update the ValidationStates group
if (this.hasFocus && this.isErroneous)
{
VisualStateHelper.GoToState(this, useTransitions, VisualStateHelper.InvalidFocused, VisualStateHelper.InvalidUnfocused);
}
else if (this.isErroneous)
{
VisualStateHelper.GoToState(this, useTransitions, VisualStateHelper.InvalidUnfocused);
}
else
{
VisualStateHelper.GoToState(this, useTransitions, VisualStateHelper.StateValid);
}
}
Cómo se puede ver, dependiendo de si el control tiene foco o no, se pasa a los distintos estados inválidos en caso de existir errores. Caso de no haberlos, se pasa a un estado de validez que desactiva los adornos de estilo informativos.
El resultado es un control que se comporta igual que los textbox por defecto de Silverlight, a pesar de ser totalmente personalizado. En cualquier caso, cambiar el aspecto que mostrará al entrar en estado erróneo es tan sencillo como modificar el estilo.
PD: Perdonad los comentarios en inglés en el código, pero es para un proyecto de la empresa en la que trabajo y la pereza me impide ponerme a traducirlo todo :)
Unos cuantos apuntes rápidos sobre Silverlight y Data binding, más a título personal (para no olvidarme de estos recursos y estas conclusiones) que con ánimo de salvarle la vida a nadie (no he conseguido ni arreglar mis propios problemas…).
Usar la propiedad DataContext de los objetos para enlazarlos con objetos del CLR (sources). Permite herencia hacia abajo y también se hereda de los controles padre, por lo que si no queremos que un objeto tenga el mismo valor que su padre, habrá que sobreescribirla.
La propiedad ElementName nos vale para enlazar a otros controles XAML en lugar de a un objeto del CLR
La propiedad RelativeSource para servir para enlazar a elementos en un Control Template.
Es necesario implementar INotifyPropertyChanged en los objetos que queramos que funcionen como sources, si queremos tener TwoWay en el binding. Si lo queremos hacer con una colección, tendrá que implementar INotifyCollectionChanged. En cualquier caso, la colección ObservableCollection<T> ya lo hace, con lo que es una buena opción para no reinventar la rueda.
Si queremos que nos aparezcan esos cartelitos rojos tan chulos con los mensajes de error que hayamos definido (aquí entra en juego los Data Annotations, estupendos para poder hacer validaciones más finas), tenemos que marcar a true las propiedades ValidatesOnException y NotifyOnValidationError del binding, en el fragmento XAML.
El punto en que me encuentro ahora es que tengo una validación correcta, pero al estar usando un control con su propio manejo de los estados, no es capaz de mostrar los errores (más bien, imagino que pasar a un estado “erróneo” que, por defecto en SL, tendrá asociada una transición que hace aparecer la caja con el error y demás artificios visuales).
Por romper mi silencio reciente (cosas de la ausencia de inspiración), un vídeo espectacular que he descubierto vía el blog corporativo de Microsoft Ibérica. Trata sobre Bing Maps y, en general, sobre la realidad aumentada, la próxima revolución en cuanto a información se trata.
Lo reconozco, me encanta la realidad aumentada. Creo que la era de la información en bruto pero desordenada se termina, y el contexto va a ser la próxima revolución.
Lástima que a día de hoy los servicios que podemos ver en el vídeo no se ofrezcan sobre ciudades españolas. Aunque, al ritmo que progresamos en nuevas tecnologías en este país, lo mismo lo tenemos para… 2020.
Hoy mismo hablaba con un compañero del trabajo sobre los ficheros svc, esos simpáticos amigos que nos permiten identificar la dirección sobre la que tiene que consumirse un servicio web en WCF. Nuestra conversación giraba en torno a la posibilidad de hostear un servicio WCF en una dirección que terminara en .asmx, para reemplazar un servicio web ASP.NET “de los antiguos” por uno de WCF sin que la URL cambiara. ¿Se podrá, pensábamos? Seguramente sí, pero a partir de WCF4 es seguramente más fácil que nunca gracias a la activación de servicios WCF sin fichero .svc.
¿Cómo hacerlo? Tirando, como no, del app.config. Como siempre, lo más fácil es verlo con un ejemplo. Vamos a partir de un proyecto WCF de ejemplo, de los que nos crea Visual Studio 2010. El contrato de servicio tendrá un método GetData y habrá una clase que implemente dicho servicio. En realidad, nos da un poco igual para lo que vamos a ver.
Vamos al tema. Nuestro fichero app.config tendrá un aspecto parecido a éste:
Como ya hemos comentado en post anteriores, la configuración se reduce a la mínima expresión en WCF4. En este caso estamos dejando al framework que se encargue de generarnos los bindings por defecto, y simplemente le indicamos que vamos a tener la dirección virtual con un “fichero” svc que activará el servicio indicado en el atributo “service”.
De este modo, cuando nos dirijamos a la dirección http://dominio/RutaServicio/MyVirtualSvcFile.svc veremos la clásica pantalla de WCF indicándonos que no tenemos los metadatos activados, como se muestra en la siguiente imagen:
Todo esto es muy bonito, pero no responde a la pregunta original: ¿podríamos usar una URL terminada en .asmx para activar el servicio WCF? Pues, al menos con esta aproximación mediante las nuevas configuraciones de WCF4… NO. Éste es el bonito error que obtenemos cuando lo intentemos:
Una pena. En cualquier caso, esta nueva forma de activar los servicios nos ahorrará tener que andar preocupados de crear N ficheros .svc, tantos como servicios queramos activar. Mucho mejor tenerlos convenientemente descritos en el fichero app.config.
Cuando me estaba preparando el MCTS sobre WCF leí una afirmación sorprendente en el Training Kit oficial de Microsoft. En él venían a decir sus autores, en una traducción más o menos libre, lo siguiente sobre el patrón de intercambio de mensajes OneWay:
Dada la naturaleza OneWay del canal, uno podría pensar que, tan pronto el consumidor envía el mensaje, éste se procesa asíncronamente y el cliente es libre de hacer otras cosas. Sin embargo, la forma en que la maquinaria de WCF funciona significa que el consumidor, de hecho, se bloquea, incluso si el mensaje es OneWay, hasta que el dispatcher entrega el mensaje a una instancia del servicio, en la forma de una llamada a un método del objeto.
Teniendo en cuenta que OneWay es un patrón “Fire-and-Forget”, no encaja mucho que el cliente tenga que mantenerse bloqueado hasta que el servidor entrega el mensaje a un objeto que implemente el contrato de servicio. Es importante recordar que alguna de las particularidades de OneWay no hacen sino resaltar esta naturaleza “Fire-and-Forget”; por ejemplo, las siguientes dos propiedades:
Un método OneWay no soporta retornar ningún resultado, siempre será un método void. Tampoco soporta el atributo FaultContract, pues no pueden definirse errores que vaya a devolver al no tener capacidad de devolver nada.
Un método OneWay no soporta fluir transacciones entre el cliente y el servidor, y viceversa.
No parece que lo afirmado por los autores del Training Kit encaje mucho con estas propiedades (y otras que se quedan en el tintero) de OneWay. Así que lo mejor es salir de dudas con un pequeño ejemplo, para ver la validez de dicho comentario.
La forma más sencilla de poder comprobarlo es aprovecharse de la forma en que está definido el pipeline de WCF. Son varios los puntos de extensibilidad que tiene este framework. Si colocáramos en uno de ellos una clase que retuviera la entrega del mensaje a un objeto servidor, podríamos comprobar si efectivamente el cliente sigue bloqueado. Para comprobarlo, vamos a usar un Message Inspector.
Inyectar un Message Inspector
Vamos a partir de un nuevo proyecto de librería WCF. La estructura que nos genera Visual Studio nos vale; dejaremos únicamente un método que no devuelva nada, como se ve a continuación.
/// <summary>
/// One Way Service.
/// </summary>
[ServiceContract]
public interface IService
{
/// <summary>
/// OneWay method.
/// </summary>
/// <param name="value">Value to send.</param>
[OperationContract(IsOneWay=true)]
void SendData(int value);
}
La implementación de este contrato de servicio no tiene demasiada importancia, con saber que deberíamos colocar un breakpoint al comienzo del método para saber en qué momento es realmente invocado, es suficiente.
Vamos ahora a empezar a crear el Message Inspector. Crearemos una clase que implemente la interfaz IDispatchMessageInspector e implementaremos uno de sus métodos.
/// <summary>
/// My message inspector class.
/// </summary>
public class MyMessageInspector : IDispatchMessageInspector
{
public object AfterReceiveRequest(
ref System.ServiceModel.Channels.Message request,
System.ServiceModel.IClientChannel channel,
System.ServiceModel.InstanceContext instanceContext)
{
// Crear un "replicador" de mensajes y usarlo para obtener una copia del mismo
MessageBuffer buffer = request.CreateBufferedCopy(int.MaxValue);
string messageContent = buffer.CreateMessage().GetReaderAtBodyContents().ReadOuterXml();
System.Diagnostics.Debug.WriteLine(messageContent);
// Asignar una copia sin leer del mensaje en request, para que otros
// componentes del pipeline de WCF puedan leerlo sin fallar.
request = buffer.CreateMessage();
// Devolver null como resultado, que será lo que reciba el metodo BeforeSendReply
// en el parámetro correlationState
return null;
}
public void BeforeSendReply(
ref System.ServiceModel.Channels.Message reply,
object correlationState)
{
}
}
En cuanto al método AfterReceiveRequest, se ejecuta en el camino de subida del mensaje desde la red hacia el objeto que va a servir esta petición. Queremos mostrarlo en la consola, para lo cual vamos a crear un buffer y generar una copia del mismo. Es importante usar el buffer en primer lugar, puesto que un mensaje no puede leerse dos veces. Si lo hiciéramos con el parámetro request, luego no podría leerse otra vez por otros componentes del pipeline de WC y cada petición fallaría irremediablemente.
En cuanto al método BeforeSendReply, no queremos hacer nada especial con él así que simplemente dejamos pasar el mensaje reply, sin leerlo para no tener el problema antes comentado de lecturas. Como curiosidad, decir que lo que devuelve el método AfterReceiveRequest lo recibe el método BeforeSendReply en su parámetro correlationState, para poder relacionar ambas llamadas entre sí.
Ahora que ya tenemos el Inspector, es el momento de modificar la configuración de WCF para usarlo. Para ello tenemos que crear una clase que modifique el comportamiento del endpoint sobre el que vamos a escuchar. Para ello, tendremos que implementar la interfaz IEndpointBehavior.
/// <summary>
/// Custom endpoint behavior.
/// </summary>
public class MyEndpointBehavior : IEndpointBehavior
{
public void AddBindingParameters(ServiceEndpoint endpoint,
System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(ServiceEndpoint endpoint,
System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
{
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint,
System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
{
endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new MyMessageInspector());
}
public void Validate(ServiceEndpoint endpoint)
{
}
}
De esta interfaz, como se puede observar, sólo nos interesa implementar el método ApplyDispatchBehavior, puesto que nuestro MessageInspector sólo va a actuar en el lado del servidor. Es importante no dejarnos ningún método con su implementación por defecto, puesto que tirar una NotImplementedException en cualquiera de ello tendría consecuencias indeseadas.
Ya sólo nos queda una última clase para tener todo el código listo. Este comportamiento personalizado para el endpoint necesitamos configurarlo de algún modo. La forma más limpia siempre es a través del fichero app.config, pero para ello necesitamos representar este comportamiento personalizable como un elemento de configuración. Esto podemos hacerlo creando una clase que herede de BehaviorExtensionElement, clase que por otra parte cargará nuestro comportamiento custom para el endpoint. El código sería algo así:
/// <summary>
/// Custom behavior extension element for the custom endpoint behavior.
/// </summary>
public class MyBehaviorExtensionElement : BehaviorExtensionElement
{
public override Type BehaviorType
{
get { return typeof(MyEndpointBehavior); }
}
protected override object CreateBehavior()
{
return new MyEndpointBehavior();
}
}
¿Sencillo, verdad? La clase simplemente representa al comportamiento personalizado que deseamos añadir como configuración. ¿Y ahora esto cómo se usaría? Primero se cargaría la clase que acabamos de definir como una extensión de los behaviors, y posteriormente se definiría como nueva configuración para un endpoint. Finalmente se cargaría en el endpoint correspondiente esta nueva configuración. Sin embargo, con las novedades que trae WCF4 en la configuración, este último paso podemos saltárnoslo, sabiendo que a partir de ese momento todos los endpoints van a tener esa extensión cargada.
Como se puede ver, en primer lugar se carga la extensión y, a continuación, se define como parte del comportamiento de los endpoints. Al no darle nombre al endpointBehavior, se cargará para todos los endpoints existentes. Por último, he activado la generación del fichero WSDL para poder generar un proxy.
Probando el inspector
Bien, suponiendo que ya tenemos el servicio arriba y un proyecto de cliente correctamente creado. Tras añadir la referencia al servicio, podemos escribir algo como lo siguiente para ver si realmente nuestro cliente se bloquea o no.
using (ServiceReference.ServiceClient proxy = new ServiceReference.ServiceClient())
{
Console.WriteLine("Calling the remote server...");
proxy.SendData(int.MaxValue);
Console.WriteLine("Remote server called...");
Console.ReadLine();
}
Con esto, es momento de lanzar el cliente. Deberemos tener breakpoints tanto en el método que hemos implementado del inspector, como en la clase que implementa el servicio. Si todo va bien veremos la siguiente secuencia de pasos:
El cliente invoca al servicio usando el proxy.
Salta el breakpoint en el MessageInspector. Si ejecutamos cualquiera de las instrucciones del inspector, con ejecución paso a paso, veremos que inmediatamente salta el breakpoint del cliente. Por lo tanto, no se está bloqueando.
El servidor se queda esperando a que sigamos la ejecución, para llegar al objeto servidor, donde el otro breakpoint debería saltar.
He probado el mismo código con las mismas condiciones (el servicio desplegado en IIS 7.0), pero con el framework anterior y el resultado es el mismo. Parece, por tanto, que OneWay sí es realmente un patrón “Fire-and-Forget” y no bloquea a los clientes