Hay diferentes formas de incrustar una aplicación Silverlight en una Web, podemos ocupar toda la web con la aplicación, en cuyo caso no nos sería útil este manual. O podemos realizar varias aplicaciones colocadas en diferentes partes de nuestra web. Para los que necesiten que sus pequeñas aplicaciones Silverlight se comuniquen, están leyendo el manual idóneo.
Si pensamos un poco el funcionamiento de una aplicación Silverlight, al fin y al cabo tiene que poder llamar a funciones JavaScript, de lo contrario, ¿Cómo lanzaría un alert en una Web?¿Cómo sería capaz de responder a eventos?
Pues sí, debajo de la capa tan bonita que tenemos en Silverlight mediante la cual programamos todas las acciones producidas en la Web en código C# o VB hay unas bibliotecas que traducen ese C# o VB compilado a código entendible por la Web, por ejemplo, JavaScript.
Sin más explicaciones de arquitectura de Silverlight, cabe decir que Silverlight está preparado para ejecutar funciones JavaScript y para permitir que sus funciones sean invocadas desde código JavaScript.
Voy a ir poniendo pequeños ejemplos que irán formando una aplicación web, en la que habrá dos aplicaciones Silverlight comunicándose usando como puente las funciones JavaSript.
Los ejemplos los haré en C#, pero se dejarán los proyectos tanto para C# como para VB (a petición de mi amigo Dani Seara).
Requisitos (más info en http://silverlight.net/getstarted):- Visual Studio 2008- Silverlight Tools Beta 2 for Visual Studio 2008- Expression Blend 2.5 June Preview- Silverlight 2 beta 2
1. Invocar función JavaScript desde C# o VB
Supongamos este código JavaScript en una página HTML.
<script type="text/javascript">
function mostrarPantalla(cadena)
{
alert(cadena);
}
</script>
Y una aplicación Silverlight con un botón que al pulsarlo ejecuta el siguiente código:
HtmlPage.Window.Invoke("mostrarPantalla", txtEnviar.Text);
Solamente con este código ya podemos invocar la función mostarPantalla de JavaScript desde C#. El resultado es:
Viendo esta función JavaScript tan sencilla, podríamos pensar que esa función la ejecuta cualquiera, pero ¿Qué pasa si lo que queremos pasar por parámetro no es un String sino un objeto .Net llamado Mensaje con su Nombre, Fecha y Texto?
Pues serializamos el objeto.
Serializar es el proceso de codificación de un objeto con el fin de transmitirlo en red (o en este caso de Silverlight a JavaScrip) en forma de una serie de bytes (serialización binaria) , o en formato texto legible por los humanos (XML o Json).
En nuestro caso vamos a serializar en formato texto y de los formatos de texto el que menos ocupa y el más fácil de deserializar por JavaScript es Json. Json es un formato que serializa los objetos de con la misma sintaxis con la que JavaScript crea los obetos.
Así que usando JSON estamos optimizando la cantidad de datos pasados por parámetro y el rendimiento de la deserialización.
Imaginemos que estamos haciendo un chat y necesitamos pasar un objeto Mensaje entre las dos aplicaciones Silverlight. Lo primero es etiquetar la clase Mensaje con el atributo DataContract para indicar que la clase es serializable y las propiedades de la clase con el atributo DataMember por lo mismo. Estos dos atributos pertenecen al ensamblado System.Runtime.Serialization.
[DataContract]
public class Mensaje
[DataMember]
public string nombre { get; set; }
public string fecha { get; set; }
public string texto { get; set; }
Para serializar un objeto cualquiera en Json realizamos el siguiente método extensor (.Net Framework 3.5):
/// <summary>
/// Metodo extensor que serializa cualquier objeto en JSON
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public static string ToJson(this object obj)
using (MemoryStream ms = new MemoryStream())
DataContractJsonSerializer serializer = new DataContractJsonSerializer(obj.GetType());
serializer.WriteObject(ms, obj);
ms.Position = 0;
using (StreamReader reader = new StreamReader(ms))
return reader.ReadToEnd();
Tener en cuenta que para poder usar la clase DataContractJsonSerializer tenemos que añadir al proyecto la referencia:
System.ServiceModel.Web => using System.Runtime.Serialization.Json;
Aunque parezca raro, el using de arriba va asociado con esa referencia, me costó bastante averiguarloJ.
Hasta ahora, ya sabemos cómo llamar desde Silverlight a una función JavaScript, incluso si los parámetros de la función son objetos complejos. Vamos a ver un ejemplo:
function enviarMensajeObjeto(mensaje)
//El formato JSON coincide que tiene la misma sintaxis que la
//creacion de objetos en JavaScript, por eso es facil crear el //objeto simplemente usando eval.
var jsMensaje = eval('(' + mensaje + ')');
alert(mensaje);
Esta función JavaScript de arriba recibe como parámetro un string con el objeto Mensaje serializado en Json y para guardarse el objeto en una variable de tipo Mensaje en JavaScript solamente tiene que usar la función eval, que evalúa una cadena de texto como si de código JavaScript se tratase.
private void enviarMensaje(Mensaje msg)
HtmlPage.Window.Invoke("enviarMensajeObjeto", msg.ToJson());
La función de Silverlight es simplemente una invocación a la función JavaScript serializando la variable msg, antes de enviarla como parámetro.
El resultado al invocar a la función enviarMensaje desde Silverlight es el Mensaje en Json:
2. Invocar función C# o VB desde JavaScript
Ahora vamos a aprender lo contrario, queremos llamar desde JavaScript a una función Silverlight.
Para poder invocar una función Silverlight primero tendremos que poder referenciar al UserControl (Page) que es el objeto que tendrá el método que queremos invocar.
Para referenciar desde Silverlight el UserControl tenemos que hacer varias cosas:
a) Declarar la clase Page (UserControl) como ScriptableType y el/los métodos que queremos invocar desde JavaScript etiquetarlos con el atributo ScriptableMember:
[ScriptableType]
public partial class Page : UserControl
[ScriptableMember]
public void setMensaje(ScriptObject scriptPerson)
Mensaje mensaje = scriptPerson.ConvertTo<Mensaje>();
listaNombres.AddMensaje(mensaje);
//Constructor, resto de métodos, etc...
Nota:
Hay que hacer notar que el parámetro de la función setMensaje no es un objeto Mensaje, sino ScriptObject. Y es que todos los objetos que se vallan a pasar desde JavaScript son ScriptObject, aunque internamente tenga la misma estructura que el objeto Mensaje, de ahí que la conversión sea automática con el método ConverTo.
b) Exponer la instancia del objeto Page al navegador y darle un nombre con el que será llamado desde JavaScript (Page también).
public Page()
InitializeComponent();
HtmlPage.RegisterScriptableObject("Page", this);
Una vez hecho esto ya podemos invocar al método setMensaje desde JavaScript con la siguiente función:
function enviarMensajeS1(mensaje)
//Se obtiene el objeto Silverlight con getElementByID
var silverlight1 = getSilverlight1();
if(silverlight1)
//Llamada a la funcion de Silverlight 2
silverlight1.content.Page.setMensaje(jsMensaje);
Esta función será invocada desde la Aplicación Silverlight 2, se pasará como parámetro un objeto Mensaje serializado con Json, y la función creará el objeto Mensaje con eval y después llamará a la función setMensaje pasándole como parámetro el objeto creado por JavaScript que será de tipo (.Net) ScriptObject.
Como conclusión de todo este documento, cabe destacar que todo esto explicado tiene muchas utilidades: comunicar dos aplicaciones Silverlight, o comunicar una aplicación Silverlight con una Flash, o simplemente Silverlight con JavaScript.
En concreto el ejemplo que he implementado es la comunicación de dos aplicaciones Silverlight y está tanto en C# como en VB.
Para explicar la interacción entre Silverlight y Html vamos a construir una aplicación de ejemplo. Dicha aplicación Silverlight tendrá dos botones, uno para obtener el nombre de usuario, mostrarlo con un alert y otro para escribir un saludo con su nombre en un div html.
Vamos a ver paso a paso como se crea esta aplicación Silverligth.
Requisitos (mas info en http://silverlight.net/getstarted):- Visual Studio 2008- Silverlight Tools Beta 2 for Visual Studio 2008- Expression Blend 2.5 June Preview- Silverlight 2 beta 2
1) Creamos un nuevo proyecto de Silverlight.
Cuando creamos un proyecto Silverlight nos pregunta si queremos crear un proyecto web que albergue la aplicación Silverlight o si se generrará al vuelo la pagina web que mostrará la aplicación Silverlight.
Le decimos que se cree dinámicamente (al vuelo)
2) Crear la página HTML que albergará la aplicación Silverlight
Para ello copiamos este código:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml" ><head> <title>Prueba de interacción entre DOM y Silverlight</title> <style type="text/css"> html, body { height: 100%; overflow: auto; } body { padding: 0; margin: 0; } #silverlightControlHost { height: 100%; } </style>
</head><body> <div id="lineaInfo">Bienvenido <span id="nombreUsuario">jquinto</span></div> <div id="saludo"></div> <div id="silverlightControlHost"> <object data="data:application/x-silverlight," type="application/x-silverlight-2-b2" width="100%" height="100%"> <param name="source" value="Bin/Release/DomSilverlight.xap"/> <param name="background" value="white" />
<a href="http://go.microsoft.com/fwlink/?LinkID=115261" style="text-decoration: none;"> <img src="http://go.microsoft.com/fwlink/?LinkId=108181" alt="Get Microsoft Silverlight" style="border-style: none"/> </a> </object> <iframe style='visibility:hidden;height:0;width:0;border:0px'></iframe> </div></body></html>
Y lo pegamos en un archivo que lo llamaremos "DomSilverlightTestPage.html" y lo guardaremos en la carpeta principal del proyecto:
Ahora para poder ejecutarlo en el proyecto como página de prueba tenemos que añadirlo:
Y una vez añadido aparecerá así:
A partir de ahora ya tenemos la página de prueba creada, con los dos div que nos interesan (uno para coger datos y otro para escribir datos desde Silverlight). Ahora vamos a hacer que el proyecto DomSilverlight cada vez que le damos a ejecutar (F5) ejecute esta página y no la que ejecuta predeterminadamente.
Vamos a las propiedades del proyecto:
Y en la pestaña Debug cambiamos la acción de comienzo y ponemos la página que nos interesa:
NOTA: Hay que tener en cuenta que esa página la hemos creado nosotros y que tiene rutas establecidas por ejemplo, presupone que el archivo "DomSilverlight.xap" (que es la aplicación Silverlight comprimida) está en la ruta Bin/Release, por lo tanto solo funciona si el modo de ejecución que tenemos es release, si queremos ejecutar en Debug tenemos que cambiar esa ruta en el HTML de prueba.
3) El código de la aplicación.
El código se divide en dos archivos, como ya sabemos Silverlight, al igual que WPF, separa el diseño del código. El diseño estará en un fichero XAML y el código en este caso en su correspondiente fichero .cs (C#).
XAML:
<UserControl x:Class="DomSilverlight.Page" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Width="400" Height="50"> <Grid x:Name="LayoutRoot"> <Grid.ColumnDefinitions> <ColumnDefinition Width="200px" /> <ColumnDefinition Width="200px" /> </Grid.ColumnDefinitions>
<Button x:Name="botonLeerDOM" Content="Obtener Nombre Usuario" Click="botonLeerDOM_Click" /> <Button x:Name="botonEscribirDOM" Content="Poner saludo en HTML" Click="botonEscribirDOM_Click" Grid.Column="1"/> </Grid></UserControl>
CS:
using System;using System.Windows.Browser;using System.Windows.Controls;using System.Windows;
namespace DomSilverlight{ public partial class Page : UserControl { HtmlElement leer; HtmlElement escribir; string nombreUsuario = "";
public Page() { InitializeComponent();
//Obtengo los elementos HTML try { leer = HtmlPage.Document.GetElementById("nombreUsuario"); escribir = HtmlPage.Document.GetElementById("saludo"); } catch (Exception ex) { HtmlPage.Window.Alert("Error: " + ex.Message); } }
/// <summary> /// Interacción de DOM a Silverlight /// </summary> /// <param name=sender"></param>" /// <param name=e"></param>" private void botonLeerDOM_Click(object sender, RoutedEventArgs e) { try { nombreUsuario = leer.GetProperty("innerHTML").ToString(); HtmlPage.Window.Alert("Nombre de Usuario: " + nombreUsuario); } catch (Exception ex) { HtmlPage.Window.Alert("Error: " + ex.Message); } }
/// <summary> /// Interacción de Silverlight a DOM /// </summary> /// <param name=sender"></param>" /// <param name=e"></param>" private void botonEscribirDOM_Click(object sender, RoutedEventArgs e) { try { escribir.SetProperty("innerHTML", "Hola mi nombre de usuario es " + nombreUsuario);
} catch (Exception ex) { HtmlPage.Window.Alert("Error: " + ex.Message); } } }}
El resultado de todo esto es que cuando le damos en el VS2008 a ejecutar nos sale esto:
Si pulsamos el boton de obtener (desde HTML hacia Silverlight).
Si pulsamos el boton de Poner (desde Silverlight hacia HTML).
Pues sabiendo esto ya podemos realizar la comunicación entre dos aplicaciones Silverlight usando como puente el HTML.
El proyecto completo esta aquí.
El pasado día 21 de abril en Madrid y el día 22 de abril en Barcelona tuvo lugar el evento Frameworks y herramientas RAD de código abierto y propietario para soluciones .NET. Donde presenté nuestro Solid RAD, una herramienta para el desarrollo rápido de aplicaciones centradas en datos con una arquitectura de dos y tres capas.
Si estáis interesados en Solid RAD Tenéis disponibles unos vídeos en mms://solidq.com/SolidRad .
NOTA: copiad y pegad mms://solidq.com/SolidRad en la barra de direcciones de IE para ver los vídeos.
La forma en la que se añade un menú personalizado ha variado en la nueva versión de Visual Studio 2008. El SDK de extensibilidad de Visual Studio 2008 ha variado la forma en la que se generan menús y comandos de forma que ya no se utilizan los "antiguos" ficheros de configuración .ctc.
Como pequeña introducción, os digo que en la nueva versión se ha optado por un nuevo sistema de configuración más acorde a los tiempos que corren basado en XML. Dicho sistema de configuración está completa y exquisitamente explicado en el siguiente enlace (no apto para lectura al final del dia;).
Dicho esto, os aviso de antemano que la documentación que viene con la propia descarga del ejecutable SDK Extensibility 1.0 para Visual Studio 2008 es errónea y hace referencia al antiguo sistema de configuración (por lo menos en lo que nos atañe que son los menús en DSL). El antiguo sistema de agregación de menús personalizados en nuestros lenguajes de dominio está explicado bastante bien en el blog de "El Bruno" o en la propia documentación que viene con el descargable de extensibilidad que he mencionado.
Vamos a ponernos manos a la obra. Para ello parto de que tienes ya tu proyecto de lenguaje de especificación de dominio creado…
Tal cual está, podemos crear nuestro menú de una forma bastante más sencilla que antes siguiendo los siguientes pasos:
En dicho archivo es donde vamos a tener que añadir los elementos del menú que queremos. En nuestro caso vamos a crearnos un botoncito que nos diga la figura que hemos clickeado.
La forma de crear nodos dentro del archivo está "explicada" muy por encima en los documentos de ayuda de mdsn (por lo menos ahora cuando escribo este post) por lo que seguramente acabarás antes viéndolo desde aquí ;)
Básicamente lo que vamos a hacer es definirnos un botón y una identificación del mismo. Importante a tener en cuenta son los nodos <Commands/> y <Symbols/> (no quiero liaros con el nodo <Extern/> que no vamos a tocar ahora)
<Buttons>
<Button guid="cmdDimeNombreFiguraGUID" id="cmdDimeNombreFiguraID type="Button">
<Parent guid="guidCmdSet" id="grpidContextMain"/>
<Strings>
<CanonicalName>cmdDimeNombreFigura</CanonicalName>
<ButtonText>Nombre figura</ButtonText>
<ToolTipText>Dice el nombre de la figura seleccionadaToolTipText>
</Strings>
</Button>
</Buttons>
<Symbols>
<GuidSymbol name="cmdDimeNombreFiguraGUID" value="{D5A40ECA-BA87-4a92-B6A7-A36C27C858AE}">
<IDSymbol name="cmdDimeNombreFiguraID" value="1"/>
</GuidSymbol>
</Symbols>
El valor del GuidSymbol ha sido generado mediante la aplicación guidgen.exe
El valor del IDSymbol puede ser decimal o hexadecimal (por ejemplo, valores 0x104 pueden darse) y podemos darle el que queramos.
Una vez realizado esto, el fichero queda de la siguiente manera:
<?xml version="1.0" encoding="utf-8"?>
<CommandTable xmlns="http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- -->
<!-- This file contains custom command definitions. -->
<!-- NOTE: Each time commands are added or changed, the "version" parameter to the -->
<!-- ProvideMenuResource attribute in Shell\Package.tt should be incremented. -->
<!-- This causes Visual Studio to re-merge the menu definitions for the package. -->
<!-- Otherwise, changes won't take effect until the next time devenv /setup is run. -->
<Extern href="stdidcmd.h"/>
<Extern href="vsshlids.h"/>
<Extern href="msobtnid.h"/>
<Extern href="virtkeys.h"/>
<Extern href="DSLToolsCmdID.h"/>
<Include href="GeneratedCode\GeneratedVsct.vsct"/>
<Commands package="guidPkg">
<Button guid="cmdDimeNombreFiguraGUID" id="cmdDimeNombreFiguraID" type="Button">
<ToolTipText>Dice el nombre de la figura seleccionada</ToolTipText>
</Commands>
</CommandTable>
Para ello, lo editamos y reemplazamos lo que viene por defecto ( [VSShell::ProvideMenuResource("1000.ctmenu", 1)] ) , por [VSShell::ProvideMenuResource("1000.ctmenu", 2)]
Y le añadimos los métodos OnPopUpMenuClick(), OnPopUpMenuDisplayAction() y GetMenuComands() a la clase.
Evidentemente lo que no vamos a hacer es escribir el código directamente sobre el fichero CommandSet.cs puesto que dicho fichero se machaca cuando le damos a "Transform All Templates" por lo que haciendo uso de las posibilidades de definición de clases parciales, le definiremos el código en un nuevo fichero.
NOTA: En mi caso, mi lenguaje se llama LenguajeOOMM y por eso la clase generada en CommandSet.cs se llama así. Por seguir una nomenclatura estándar he creado dicho nombre al fichero. En cualquier caso, el contenido siempre ha de ser el de la definición de la clase doblemente derivada.
Antes hacíamos referencia al fichero CommandSet.cs. Por poco que lo abramos y le demos un vistazo veremos que sigue la pauta de clase doblemente derivada (que se sale del tema del post pero que en futuros post trataré) que crea una clase heredada de DslShell::CommandSet (en mi caso llamada LenguajeOOMMCommandSetBase), y luego otra clase que hereda de la anterior y es a la que vamos a añadirle la funcionalidad citada en el paso 6.
using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
using Microsoft.VisualStudio.Modeling.Shell;
using System.Collections;
using System.Text;
namespace LenguajeOOMM
/// Double-derived class to allow easier code customization.
internal partial class LenguajeOOMMCommandSet : LenguajeOOMMCommandSetBase
protected override IList<System.ComponentModel.Design.MenuCommand> GetMenuCommands()
IList<System.ComponentModel.Design.MenuCommand> commands = base.GetMenuCommands();
DynamicStatusMenuCommand cmdDimeNombreFigura =
new DynamicStatusMenuCommand(
new EventHandler(OnPopUpMenuDisplayAction),
new EventHandler(OnPopUpMenuClick),
new CommandID(new Guid("D5A40ECA-BA87-4a92-B6A7-A36C27C858AE"), 1));
commands.Add(cmdDimeNombreFigura);
return commands;