Patrones de Diseño: Observer (y II)

En anteriores entradas hemos visto cómo implementar el patrón de diseño Singleton y también de qué modo tan práctico encontramos el patrón de diseño Observer implementado en las características incluidas de serie en Xojo. En esta entrada pondremos todo esto en práctica creando nuestro propio Centro de Notificaciones: una clase que permite compartir una única instancia entre toda la aplicación (Singleton), y sobre la cual pueden registrarse aquellos objetos que deseen recibir notificaciones de un objeto determinado y para uno o varios mensajes (notificaciones).

Además, la clase también permite desuscribir cualquiera de los objetos previamente suscritos, ya sea para dejar de recibir notificaciones por parte del objeto (u objetos) observados, o bien sólo para determinados mensajes del objeto u objetos para los cuales se haya registrado.

Dado que los aspectos relacionados con los Singleton y el patrón de diseño Observer ya se han visto en anteriores entradas, te recomiendo que los revises para que puedas aprender los conceptos base. También conviene refrescar las Interfaces de Clase en Xojo, puesto que haremos uso de ellas en la implementación de esta solución. A continuación sólo nos ocuparemos de ver el código que varía a la hora de crear nuestra clase NotificationCenter y ponerla en práctica en un proyecto de ejemplo.

Por supuesto, antes de continuar leyendo, siempre puedes descargar el proyecto de ejemplo, ejecutarlo y revisar el código por ti mismo para obtener una primera idea de qué hace y de como lo hace. Después, puedes continuar leyendo este artículo si necesitas comprender mejor el funcionamiento tanto del Centro de Notificaciones como de las clases que hacen uso de dicho diseño.

Clase NotificationCenter

Crea un nuevo proyecto de Escritorio en Xojo y añade una nueva clase. Utiliza el Panel Inspector para cambiar el atributo name a NotificationCenter. Este es el nombre de la clase (Tipo de Dato) que estamos creando.

Con la nueva clase seleccionada en el Navegador de Proyectos (la columna situada más a la izquierda en el IDE) añade ahora una nueva Propiedad Compartida (Insert > Shared Property). En el Panel Inspector, cambia el atributo name a Instance, el campo Type (Tipo de Dato) a NotificationCenter, y el Scope (Ámbito) a Private. (Ya sabes que una Propiedad o Método Compartidos son aquellos a los que se puede acceder sobre la propia clase, sin necesidad de crear una instancia previamente.)

Añade a la clase una segunda Propiedad, en esta ocasión mediante Insert > Property (es decir, una propiedad de instancia en este caso), utilizando los siguientes valores en el Panel Inspector:

  • Name: Objects
  • Type: Xojo.Core.Dictionary
  • Scope: Private

A continuación, añade un nuevo método (Insert > Method). Utiliza nuevamente el panel Inspector para seleccionar Constructor en el menú desplegable Method Name y define el Scope (ámbito) a Private (Privado).

Introduce el siguiente código en el Editor de Código asociado con el nuevo método:

Objects = new xojo.core.Dictionary

El hecho de que sea un Constructor de ámbito privado nos interesa para que no se puedan crear varias instancias de la clase. En vez de ello tendrán que acceder al método de conveniencia definido por nosotros para recuperar la misma —y única— instancia en todos los casos.

Por tanto, ahora añadiremos un nuevo Método Compartido utilizando para ello Insert > Shared Method. Utiliza el Panel Inspector con los siguientes datos:

  • Name: SharedInstance
  • Return Type: NotificationCenter

E introduce el siguiente código en el Editor de Código asociado con el método recién creado:

if instance = nil then instance = new NotificationCenter
Return instance

Hasta ahora nos hemos ocupado de definir la estructura base de un Singleton, así como el que será nuestro modelo de datos: la propiedad Objects de tipo Xojo.Core.Dictionary, y que será el responsable de almacenar los objetos registrados (observadoress) para un Control determinado, así como los mensajes de los que está interesado recibir notificaciones para cada objeto observado. Para ello se utiliza el esquema mostrado en la siguiente imagen:

Esquema Modelo de Datos NotificationCenter

Es decir, el diccionario utiliza como Key cualquier objeto de tipo RectControl y que se corresponderá con el objeto a observar. El valor asociado con dicha Key será un nuevo diccionario, cuyas entradas utilizarán a su vez como Key la notificación (mensaje) para el cual un objeto determinado desea registrarse como observador sobre dicho control. De este modo, un mismo objeto puede estar registrado para varios controles y también para varios mensajes en uno o varios controles. Por último, el valor asociado con cada Key en este segundo diccionario será un Array cuyos contenidos serán instancias de NotificationReceiver. Así quedan reunidos todos los controles interesados en recibir notificaciones para un mismo mensaje de un mismo control.

Registrar observadores

Con la clase aun seleccionada, añade un nuevo método (Insert > Method) utilizando los valores indicados a continuación. Este será el método responsable de registrar nuevos observadores en el Centro de Notificaciones:

  • Name: Register
  • Parameters: sourceControl as NotificationReceiver, observedObject as RectControl, message as Text
  • Scope: Public

Como puedes ver en Parameters, el primer parámetro es el objeto Observador (es decir, el interesado en recibir las notificaciones); el segundo parámetro es el objeto a observar (es decir, del cual deseamos recibir notificaciones); y por último se encuentra el mensaje para el cual deseamos recibir las notificaciones por parte del objeto observado.

Introduce el siguiente código en el Editor de Código asociado con este método:

dim observedObjects as xojo.Core.Dictionary = Objects.Lookup( observedObject, new xojo.core.Dictionary )
dim messages() as NotificationReceiver
if observedObjects.HasKey( message ) then messages = observedObjects.Value( message )
messages.Append sourceControl
observedObjects.Value( message) = messages
objects.Value( observedObject ) = observedObjects

Enviar Notificaciones

Ahora nos encargaremos de añadia a la clase el método responsable de enviar las notificaciones a aquellos objetos interesados en recibirlas por parte de un control determinado. Vuelve a crear un nuevo método para la clase NotificationCenter y utiliza los siguientes valores en el Panel Inspector asociado:

  • Name: sendNotification
  • Parameters: sourceObject as RectControl, message as text, value as Auto
  • Scope: Public

Así como el siguiente código en el Editor de Código asociado:

dim observedObjects as xojo.Core.Dictionary = objects.Lookup(sourceObject, nil)
if observedObjects <> nil then
dim sourceObjects() as NotificationReceiver
if observedObjects.haskey(message) then sourceObjects = observedObjects.Value( message )
for n as integer = 0 to sourceObjects.Ubound
dim target as NotificationReceiver = sourceObjects(n)
target.valueReceived( value, message )
next
end if

Es decir, el primer parámetro del método se corresponde con un control que desea enviar una notificación (segundo parámetro) a todos los posibles objetos registrados, pasándoles el valor que podrán tratar o descartar los receptores según les interese (tercer parámetro).

Dejar de recibir Notificaciones

Ahora bien, es probable que un objeto previamente registrado desee dejar de recibir notificaciones de cualquier control. Para ello, crea un nuevo método en la clase NotificationCenter utilizando los siguientes valores:

  • Name: RemoveObserver
  • Parameters: sourceControl as NotificationReceiver

Con el siguiente código en el Editor de Código asociado al método:

for each observedObjects as xojo.Core.DictionaryEntry in objects
dim d as xojo.Core.Dictionary = objects.Value(observedObjects.Key)
for each messagesForObject as xojo.Core.DictionaryEntry in d
dim sourceObjects() as NotificationReceiver = d.Value(messagesForObject.Key)
for n as integer = sourceObjects.Ubound DownTo 0
if sourceObjects(n) = sourceControl then sourceObjects.Remove(n)
next
next
next

Dejar de recibir algunas Notificaciones

También puede darse el caso de que un objeto desee darse de baja en el Centro de Notificaciones para dejar de recibir sólo un tipo de notificación determinada y que hubiese registrado previamente. Para ello, añade un nuevo método y utiliza los siguientes valores:

  • Name: removeObserverForMessage
  • Parameters: sourceControl as NotificationReceiver, message as text

Introduciendo el siguiente código en el Editor de Código asociado:

for each observedObjects as xojo.Core.DictionaryEntry in objects
dim d as xojo.Core.Dictionary = objects.Value(observedObjects.Key)
for each messagesForObject as xojo.Core.DictionaryEntry in d
if messagesForObject.Key = message then
dim sourceObjects() as NotificationReceiver = d.Value(messagesForObject.Key)
for n as integer = sourceObjects.Ubound DownTo 0
if sourceObjects(n) = sourceControl then sourceObjects.Remove(n)
next
end if
next
next

Una Interface de Clase para todos: NotificationReceiver

A lo largo de la definición de nuestra clase NotificationCenter hemos visto en repetidas ocasiones que se hacía referencia al Tipo de Dato NotificationReceiver. Esto es lo que vamos a definir a continuación.

En realidad se trata de una Interface de Clase, de modo que sirva como pegamento para que puedan implementar los métodos definidos en ella cualquier objeto que, además de figurar como su clase nativa, también desee aparecer ante los ojos del resto del código como una instancia de tipo NotificationReceiver. De ese modo, podremos registrar como observador en nuestro Centro de Notificaciones cualquier objeto que añada esta interface de clase.

Utiliza Insert > Class Interface para crear una nueva Interface de Clase en el proyecto. A continuación, introduce los siguientes valores en el Panel Inspector asociado:

  • Name: NotificationReceiver

Con la Interface de clase seleccionaa en el Navegador de Proyecto, añade un nuevo método con la siguiente signatura en el Panel Inspector:

  • Method Name: valueReceived
  • Parameters: value as Auto, message as Text

¡Listo! En función de cuáles sean tus necesidades, puedes ampliar la lista de métodos en esta interface de clase, en combinación con la funcionalidad de la propia clase NotificationCenter.

Crear nuestro proyecto de ejemplo

Para comprobar el funcionamiento de nuestro Centro de Notificaciones, vamos a crear un proyecto donde los cambios producidos en un control de tipo TextField actualizará automáticamente los contenidos de un control Label, un Slider, un ListBox y un Canvas. Es más, la instancia de Label se suscribirá en el Centro de Notificaciones para recibir dos tipos de notificaciones, variando la información mostrada en función de cuál sea el mensaje recibido.

También añadiremos un par de controles CheckBox para comprobar la funcionalidad de desactivar todas las notificaciones en el Label o bien recibir sólo un tipo de notificación sobre dicho control.

Pero antes de nada, y como hemos visto en el paso previo, es preciso que todos los objetos que se registren en el Centro de Notificaciones sean de tipo NotificationReceiver, lo que significa que tendremos que crear nuestras propias subclases de Label, Slider, ListBox y Canvas. ¡Empecemos por el Label!

Subclase Label

Crea una subclase de Label. Puedes hacerlo seleccionando el Control Label en el panel Library y accediendo a continuación al menú contextual para seleccionar la opción New Subclass; o bien puedes arrastrarlo y soltar el control directamente sobre el Navegador de Proyectos. En cualquier caso, el resultado será el mismo: se habrá creado en el Navegador de Proyectos una nueva instancia con el nombre CustomLabel (puede variar, en función de la versión de Xojo que estés utilizando). También puedes utilizar Insert > Class, sin más.

Sea cual sea el sistema utilizado a la hora de crear la nueva clase, selecciónala en el Navegador de Proyecto y utiliza los siguientes valores en el Panel Inspector:

  • Name: MyLabel
  • Super: Label. Así, nuestra subclase heredará todos los métodos, eventos y propiedades de dicha clase.
  • Interfaces: Pulsa el botón y en la ventana resultante, selecciona la entrada correspondiente a NotificationReceiver. Confirma la selección.

Precisamente como resultado de esta última acción, se habrá añadido automáticamente a la clase el método ValueReceived. Introduce el siguiente código en el Editor de Código asociado:

RaiseEvent newValueReceived value, message

Lo que hacemos es lanzar un nuevo evento (aun por definir) de modo que sea en cada caso la propia instancia de la clase la encargada de determinar qué hacer con el valor recibido en función de cuál sea el mensaje enviado.

Por tanto, con la clase MyLabel seleccionada, utiliza Insert > Event Definition para añadir la definición de evento que necesitamos, con los siguientes valores:

  • Event Name: newValueReceived
  • Parameters: value as auto, message as Text

Subclase Slider

Crea una nueva clase y utiliza los siguientes valores en el Panel Inspector:

  • Name: MySlider
  • Super: Slider
  • Interfaces: Añade la interface de clase NotificationReceiver

Introduce el siguiente código en el método ValueReceived añadido por la Interface de clase:

RaiseEvent newValueReceived val(value)

Además de crear la definición de evento con Insert > Event Definition, utilizando la siguiente signatura en el Panel Inspector:

  • Event Name: newValueReceived
  • Parameters: value as integer

Subclase ListBox

Crearemos ahora la subclase de ListBox, siguiendo básicamente los mismos pasos con los siguientes valores:

  • Name: MyListBox
  • Super: ListBox
  • Interfaces: NotificationReceiver

Introduce lo siguiente en el Editor de Código asociado con el método ValueReceived añadido por la Interface de Clase:

RaiseEvent newValueReceived val(value)

Y crearemos la Definición de Evento correspondiente con la siguiente signatura:

  • Event Name: newValueReceived
  • Parameters: value as integer

Subclase Canvas

Llegamos a la última de las subclases de las que consta nuestro proyecto de ejemplo. Inserta una nueva clase en el proyecto y utiliza los siguientes valores en el Panel Inspector:

  • Name: MyCanvas
  • Super: Canvas
  • Interfaces: NotificationReceiver

Introduce el siguiente código en el método ValueReceived:

dim s as string = value
newValue = s.ToText
me.Refresh

A contunuación utiliza Insert > Event Handler para añadir el evento Paint a nuestra subclase. Introduce el siguiente código en el Editor de Código resultante:

dim size as integer = val(newValue)
g.TextSize = size
dim centerx as integer = g.Width/2 - g.StringWidth(newValue)/2
dim centery as integer = g.Height/2+g.StringHeight(newValue,g.Width)/2
g.ForeColor = rgb(size, size, size)
g.FillRect(0,0,g.Width,g.Height)
g.ForeColor = if(size <=50, rgb(255,255,255), rgb(0,0,0))
g.DrawString(newValue,centerx,centery)

Por último añadiremos una propiedad a la sublcase con Insert > Property, utilizando los siguientes valores:

  • Name: newValue
  • Type: Text
  • Scope: Private

Diseñar la interfaz de usuario

Con las subclases ya creadas de los objetos (en este caso Controles) que actuarán como observadores en nuestra app, nos encargaremos ahora de diseñar la interfaz de usuario. Para ello, selecciona la ventana por omisión del proyecto, Window1 para acceder al Diseñador de Plantillas y arrastra a continuación las subclases recién creadas de los controles desde el Navegador de Proyectos o la Librería para que quede de forma similar a lo que podemos ver en la siguiente imagen. Por otra parte, utiliza el Panel Librería para añadir a la interfaz tanto el campo de texto (TextField) como las casillas de verificación (CheckBox).

Interfaz de usuario, prueba NotificationCenter

Selecciona la instancia de la etiqueta (en mi ejemplo, con el nokmbre Label2) y utiliza Insert > Event Handler para añadir el manejador de evento newValueReceived que habíamos definido previamente en nuestra clase. Introduce el siguiente código en el Editor de Código resultante:

dim s as string = value
Select case message
case "valueChanged"
me.Text = "Value on source Control: "+ s
case "charValue"
me.Text = "ASCII Code for last keystroke: "+ str(asc(s))
End Select

Como puedes observar, el evento utiliza el mensaje recibido para decidir cómo ha de interpretar el valor enviado por el objeto observado. En un caso se hará literal y en el otro nos mostrará el valor ASCII correspondiente al valor recibido como texto.

Selecciona a continuación la instancia del Slider (en el ejemplo, con el nombre Slider1), y repite nuevamente la acción para añadir sobre el control el manejador de evento newValueReceived. Introduce el siguiente código en el Editor de Código asociado:

me.Value = value

En este caso, el control se limita simplemente a asignar el valor recibido.

Elige ahora la instancia correspondiente al ListBox (ListBox1 en el ejemplo). Añade el manejador de evento newValueReceived e introduce la siguiente línea de código:

me.ListIndex = value

En la instancia ListBox1 añadiremos también el manejador de evento Open, de modo que podamos contar con una serie de valores por omisión sobre el listado. Una vez añadido el evento, escribe el siguiente código en el Editor de Código asociado:

for n as integer = 0 to 99
me.AddRow str(n)
next

Por último, selecciona la instancia correspondiente al TextField (TextField1 en el ejemplo), y añade los manejadores de evento KeyDown y LostFocus. Una vez añadidos, selecciona el evento KeyDown e introduce el siguiente código:

dim myNotificationCenter as NotificationCenter = NotificationCenter.sharedInstance
myNotificationCenter.sendNotification(me,"charValue",key)

A continuación, selecciona el evento LostFocus e introduce el siguiente código:

dim myNotificationCenter as NotificationCenter = NotificationCenter.sharedInstance
mynotificationcenter.sendNotification(me,"valueChanged",me.Text)

En ambos casos, se recupera la instancia compartida del Centro de Notificaciones y se publica una nueva notificación, pasando como primer atributo el propio control que actúa como originador del mismo, a continuación el mensaje o “notificación” que publica y, por último, el valor enviado. A partir de aquí será el Centro de Notificaciones quien se encargue de propagar la información entre los objetos registrados.

Activar y desactivar Notificaciones

Ahora nos encargaremos de los controles de la interfaz de usuario dirigidos a activar o desactivar las notificaciones recibidas por parte de nuestra instancia MyLabel (Label1).

Selecciona el control CheckBox1, añade el evento Action e introduce el siguiente código en el Editor de Código asociado:

dim miNotificationCenter as NotificationCenter = NotificationCenter.sharedInstance
select case me.Value
case true
miNotificationCenter.register(Label2,TextField1, "valueChanged")
miNotificationCenter.register(Label2, TextField1, "charValue")
case False
miNotificationCenter.removeObserver(Label2)
CheckBox2.value = false
end select

Elige ahora el CheckBox2, añade el manejador de evento Action e introduce el siguiente código:

dim miNotificationCenter as NotificationCenter = NotificationCenter.sharedInstance
select case me.Value
case true
miNotificationCenter.register(Label2,TextField1, "valueChanged")
case False
miNotificationCenter.removeObserverForMessage(Label2,"valueChanged")
end select

Como puedes observar, en el primer caso, se registra el control Label2 para recibir ambas notificaciones cuando la casilla de verificación está activada, y se deregistra por completo cuando la casilla de verificación está desactivada. En el caso del control CheckBox2 sólo se registra o desuscribe, respectivamente, para una de las notificaciones.

¡Ponerlo todo en marcha!

¿Que necesitamos para ponerlo todo en marcha? Añade el manejador de evento Open a Window1 e incluye el siguiente código. Este es el encargado de registrar los diferentes controles en el Centro de Notificaciones para los mensajes en los que están interesados. Ten en cuenta que esta operación podría haberse realizado también sobre el evento Open de cada uno de los propios controles interesados:

dim miNotificationCenter as NotificationCenter = NotificationCenter.sharedInstance
miNotificationCenter.register(Label2,TextField1, "valueChanged")
miNotificationCenter.register(Label2, TextField1, "charValue")
miNotificationCenter.register(Listbox1,TextField1, "valueChanged")
miNotificationCenter.register(Slider1,TextField1, "valueChanged")
miNotificationCenter.register(canvas1,TextField1, "valueChanged")

Conclusiones

Como has podido observar, mediante la combinación de los patrones de diseño Observer y Singleton, así como mediante el uso de las Interfaces de Clase, hemos podido crear un Centro de Notificaciones flexible que automatiza y simplifica sobremanera la interrelación entre los objetos de un programa, automatizando procesos entre éstos y facilitando también el mantenimiento y extensión de la funcionalidad.

*Esta entrada ha sido escrita en Markdown y exportada como HTML para este blog con Snippery

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *