View
339
Download
2
Category
Preview:
Citation preview
Desarrollo de aplicaciones con .NET y WPF
Andrés Marzal Departamento de Lenguajes y Sistemas Informáticos
Universitat Jaume I
Decharlas, 24 de mayo de 2010
¿Qué es .NET? ¿Qué es C#? ¿Qué es WPF? ¿Qué es Visual
Studio? ¿Qué es Expression Blend? odemos simplificar mucho y decir que .NET es la respuesta de Microsoft a Java. .NET ofrece un entorno
de ejecución con máquina virtual para un lenguaje de máquina propio: IL, por Intermediate Language.
Diferentes lenguajes se traducen a ese lenguaje de máquina y un compilador de última hora genera
código nativo, que es lo que realmente se ejecuta.
.NET sigue un estándar ECMA: “Standard ECMA-335, Common Language Infrastructure (CLI)”. La
implementación de Microsoft del CLI se conoce por CLR (Common Language Runtime). Hay una
implementación libre de CLI desarrollada por Novell: Mono. Acompaña al entorno un conjunto de
librerías gigantesco, aspecto en el que .NET va significativamente por delante de Mono.
El lenguaje de preferencia para .NET es C# (se lee “C Sharp”), un lenguaje que se diseñó para superar
algunos problemas de Java. En particular, la diferencia sustancial entre valores y objetos y la carencia
de delegados que facilitaran la implementación del patrón observador/observable. C# ha evolucionado
mucho desde su aparición, pero mantiene una coherencia en el diseño que lo hace fácil de aprender.
Aunque es un lenguaje con herencia simple, implementación de interfaces y memoria con
recolección automática, como Java, se diferencia de éste en numerosos aspectos importantes. C# ha
integrado eficazmente varios conceptos de la programación funcional, como las funciones anónimas
y las clausuras. Cuenta además con un mini-lenguaje para efectuar consultas a fuentes de datos, LINQ,
que facilita mucho la gestión de información proveniente de bases de datos, de colecciones en
memoria, de ficheros XML, etcétera. Lo cierto es que LINQ facilita el trabajo con cualquier objeto que
proporcione una enumeración de elementos. Las enumeraciones son muy corrientes en .NET, pues C#
facilita su diseño e implementación mediante estructuras de control como “yield return”. C# evita,
además, la verbosidad del patrón de consulta y asignación de valor a campos (“getters & setters”) propia
de Java mediante las denominada propiedades. Finalmente cabe advertir que la implementación de
tipos genéricos en C# es mucho más sólida que la de Java, pues conserva información de tipos y
distingue entre valores y objetos en el parámetro de tipo, a diferencia de lo que ocurre en Java, que
basa su implementación de genéricos en el borrado de tipos. C# está estandarizado y su definición se
encuentra en “Standard ECMA-334 – C# Language Specification”. Va por la versión 4.0 tanto en .NET
como en Mono.
WPF son las siglas de Windows Presentation Foundation. Es un conjunto de librerías para
implementar aplicaciones interactivas. Arrancó con el nombre en clave “Avalon”. Presenta muchos
P
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
¿Q
ué e
s .N
ET?
¿Qu
é e
s C
#?
¿Qu
é e
s W
PF?
¿Qu
é e
s V
isu
al Stu
dio
? ¿Q
ué e
s Exp
ress
ion
Ble
nd
?
2
aspectos interesantes: separación de apariencia y lógica,
soporte del patrón “orden” (command), fácil conexión a
fuentes de datos vía ligaduras (bindings), simplificación
de trabajo con objetos observables mediante
propiedades de dependencia, herencia de valores para
propiedades por relación jerárquica entre componentes,
acceso directo a hardware gráfico, animaciones,
personalización completa de componentes mediante
plantillas, etcétera.
La P de WPF viene de “Presentation” y es importante.
WPF soporta el patrón arquitectónico Modelo-Vista-
Presentador (frente al clásico Modelo-Vista-Controlador).
La versión WPF de este patrón es la que se conoce por
Modelo-Vista-Modelo de la Vista, o MVVM por
Model-View-ViewModel.
Hay una versión ligera de WPF diseñada para correr
incrustada en navegadores (aunque también puede
ejecutarse fuera del navegador): Silverlight. El proyecto
arrancó con el nombre en clave WPF/E, por WPF
Everywhere, y muchas veces se habla de él en términos
de competencia directa con Flex y Flash. Mono ofrece
una implementación libre de Silverlight: Moonlight
(aunque suele ir retrasada con respecto a la de Microsoft:
La versión actual de Silverlight es la 3.0, con la 4.0 a
punto de salir, y Moonlight implementa la funcionalidad
de la 2.0 y buena parte de 3.0).
WPF propone separar apariencia de lógica y lo lleva al
extremo de ofrecer una herramienta para diseñadores
gráficos que se integra en el proceso de desarrollo.
Cuando el programador crea una interfaz gráfica se
concentra en los elementos desde el punto de vista lógico y en cómo se comunican entre sí y con los
datos de la aplicación. Los ficheros generados son directamente accesibles con Microsoft Expression
Blend. Allí, el diseñador encuentra una aplicación con la que es sencillo cambiar el aspecto visual de los
elementos, aplicar efectos y diseñar animaciones. Blend es parte de la suite Microsoft Expression, que
incluye más herramientas orientadas a diseñadores gráficos (como Microsoft Expression Design, una
herramienta en la línea de Adobe Freehand).
Visual Studio es la plataforma de desarrollo por excelencia para .NET. Su última versión es Visual Studio
2010 (VS 2010) y ofrece soporte nativo para lenguajes .NET como C#, Visual Basic, F# (un lenguaje
funcional de la familia de Ocaml) y para proyectos “clásicos” con C o C++. VS 2010 es extensible y
cuenta con soporte (de terceras partes) para herramientas como Subversion o Mercurial. El proyecto
Mono cuenta con su propia plataforma de desarrollo: MonoDevelop. Y hay otra plataforma abierta,
SharpDevelop, aunque de uso marginal.
.NET y Software Libre
El principal problema de .NET para la
comunidad no es de carácter técnico, sino el
estigma de ser obra de Microsoft. Muchas de
las herramientas libres de uso común en Java
están disponibles para .NET: NHibernate,
NAnt, NUnit, Spring.NET, etcétera. Microsoft
abrió en 2009 CodePlex, un espacio para dar
soporte a proyectos de software libre y
algunos de sus proyectos recientes se
distribuyen con licencias que permite acceder
al código fuente (MEF e IronPython, por
ejemplo). Microsoft apoya oficiosamente la
iniciativa Moonlight, de Mono, con la que se
está desarrollando una versión abierta de
Silverlight.
Aunque CLI o C# se han publicado como
estándares ECMA y la actitud de Microsoft
ante la comunidad de software libre ha
evolucionado mucho, la comunidad mira con
recelo cualquier innovación que provenga de
Microsoft. Por ejemplo, Miguel de Icaza, líder
del proyecto Mono, es frecuentemente
insultado o menospreciado por personas o
asociaciones respetadas en la comunidad del
software libre. (Se puede obtener información
sobre uno de los últimos rifi-rafes en
http://www.fsf.org/blogs/rms/microsoft-
codeplex-foundation,
http://www.linuxtoday.com/news_story.php3
?ltsn=2009-09-21-028-35-OP-CY-EV).
Parece que ahora le toca a Microsoft sufrir
una campaña de FUD como las que montaba
hace tiempo.
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Pri
mero
s p
aso
s co
n W
PF
3
Primeros pasos con WPF onstruyamos una aplicación WPF extremadamente sencilla para empezar a entender algunos de los
elementos básicos de WPF y C#. Nuestra aplicación mostrará un formulario en el que el usuario puede
poner nombre y primer apellido. Tras pulsar un botón, se mostrará un cuadro de diálogo modal con un
saludo personalizado.
Empezamos iniciando VS 2010 (Figura 1a). Con la opción FileNewProject… creamos un nuevo
proyecto de tipo “WPF Application” al que denominamos HolaMundo (Figura 1b).
Esto crea un directorio HolaMundo y una estructura de
ficheros y directorios que facilita el desarrollo de la
aplicación. El explorador de soluciones muestra esta
estructura (Figura 2). Encontramos una carpeta para
propiedades, otra para referencias a DLLs y “ficheros
lógicos” de aplicación que pueden desplegarse en uno o
más ficheros físicos.
Los ficheros App.* contienen el punto de entrada a la
aplicación y definen/construyen una instancia de la clase
Application. Los ficheros MainWindow.* definen la ventana
principal de la aplicación y App.* contiene una indicación de
que MainWindow.* es la ventana principal o URI de inicio.
Los ficheros que nos interesan tienen extensión “.xaml” o
“.cs”. Los primeros son ficheros en un formato XML
denominado XAML (eXtensible Application Markup
Language); los segundos son ficheros C#.
XAML sigue un esquema XML cuyos elementos se inscriben en el espacio de nombres
{http://schemas.microsoft.com/winfx/2006/xaml}. XAML es un lenguaje de marcado diseñado para
facilitar la instanciación de objetos .NET y la definición de sus propiedades (o “atributos”, en jerga XML).
Hay una versión de XAML con un esquema cuyos elementos están en el espacio de nombres
C
(a) (b)
Figura 1. (a) Pantalla inicial de Visual Studio 2010. (b) Cuadro de diálogo para crear un nuevo proyecto.
Figura 2. Explorador de soluciones con el
proyecto WPF recién creado.
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Pri
mero
s p
aso
s co
n W
PF
4
{http://schemas.microsoft.com/winfx/2006/xaml} que permite instanciar objetos WPF y definir sus
propiedades.
XML es un formato jerárquico y, por tanto, los objetos en XAML siempre forman un árbol. Esa
estructura es natural en una interfaz de usuario. Veamos el aspecto de App.xaml:
<Application x:Class="HolaMundo.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="MainWindow.xaml"> <Application.Resources> </Application.Resources> </Application>
El fichero instancia un objeto de la clase App (que es nuestra y hereda de otra, Application, propia de
WPF) en el espacio de nombres C# HolaMundo (lo indica el atributo x:Class). Define los dos espacios de
nombres XML que se usan: XAML y WPF y señala que la aplicación empieza en la URI
MainWindow.xaml. A continuación, define una sección para los recursos de la aplicación, pero no los
hay. La sintaxis es especial y merece que nos detengamos: la marca Application.Resources corresponde,
en realidad, a un atributo “Resources” de la marca “Application”. Es decir, en principio (pero sólo en
principio), podríamos haber encontrado algo de este estilo:
<Application x:Class="HolaMundo.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="MainWindow.xaml" Resources="…"> </Application>
¿Por qué se usa esa otra sintaxis, extraña en el mundo XML (aunque siga el estándar)? Porque así es
posible que el atributo Resources contenga nuevos elementos XML y no sólo una simple cadena. Esta
sintaxis alternativa es muy corriente en los ficheros XAML y conviene acostumbrarse a ella.
El fichero App.xaml.cs no contiene gran cosa:
using System; using System.Collections.Generic; using System.Configuration; using System.Data; using System.Linq; using System.Windows; namespace HolaMundo { /// <summary> /// Interaction logic for App.xaml /// </summary> public partial class App : Application { } }
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Bo
ton
es
y e
ven
tos
5
La clase App, en el espacio de nombres HolaMundo, es una clase parcial (adjetivo “partial”), lo que
significa que parte de su código está definido en otro fichero. Ese otro fichero contiene el código auto-
generado por VS 2010.
La clase está vacía (en nuestro fichero, pero no en el auto-generado). En nuestra parte podríamos
definir, por ejemplo, comportamientos relacionados con el ciclo de vida de la aplicación (creación,
activación, minimización, cierre, etcétera).
Veamos ahora qué contiene MainWindow.xaml:
<Window x:Class="HolaMundo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid> </Grid> </Window>
Se crea una instancia de un objeto de la clase MainWindow, que por herencia es también de la clase
Window. Se definen algunos atributos: el título (Title), la altura (Height) y la anchura (Width). Las
unidades en que se indican las medidas son independientes de la pantalla. Una pulgada corresponde
siempre a 96 unidades. (Se escogió esta unidad de medida por facilitar la medida en monitores
convencionales, que suelen presentar una resolución de 96 dpi, es decir, 96 puntos por pulgada.)
Dentro de la ventana hay un Grid. Es un elemento de maquetación. Más adelante lo estudiaremos con
detenimiento.
Ejecutemos la aplicación (pulsamos la tecla F5) y aparece una ventana vacía (Figura 3). VS 2010 sigue
estando accesible, pero no permite editar el contenido del proyecto. Cerramos la aplicación pulsando
en el botón de cierre de la ventana (o pulsando Shift-F5 en VS 2010) y volvemos a VS 2010.
Botones y eventos camos a crear un botón con el texto “Saluda” y haremos que al pulsarlo aparezca una ventana modal
con un saludo:
<Window x:Class="HolaMundo.MainWindow" … Title="MainWindow" Height="350" Width="525"> <Grid> <Button Click="Button_Click"> Saluda </Button> </Grid> </Window>
El texto “Saluda” es el valor que se asigna a una
propiedad por defecto: Content. Es decir, este
código XAML es equivalente:
V
Figura 3. Ventana de la aplicación en ejecución.
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Bo
ton
es
y e
ven
tos
6
<Window x:Class="HolaMundo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid> <Button Content="Saluda" Click="Button_Click" /> </Grid> </Window>
Algunos objetos tienen campos privilegiados en tanto que el contenido de la marca XML (el texto o
código XML que va entre las marcas de apertura y cierre) se les asigna automáticamente. En un botón,
el campo especial es Content, en una caja de texto, el campo especial es Text.
El atributo Click corresponde a un evento. Cada vez que se pulse en el elemento de tipo Button con el
botón izquierdo del ratón y se levante en el interior del elemento, se invocará automáticamente al
método Button_Click. El asistente de VS 2010 nos ha preparado el método Button_Click en
MainWindow.xaml.cs:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace HolaMundo { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void Button_Click(object sender, RoutedEventArgs e) { } } }
El parámetro sender de Button_Click contendrá una referencia al propio botón y el parámetro de tipo
RoutedEventArgs contendrá ciertos datos relativos al evento cuando éste ocurra. Vamos a rellenar el
método con la invocación al diálogo modal:
private void Button_Click(object sender, RoutedEventArgs e) {
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Maq
ueta
ció
n c
on
pan
ele
s
7
MessageBox.Show("Hola"); }
El código que acompaña al XAML se denomina “código trasero” (code behind). Y aunque ahora
recurrimos a él, veremos que MVVM permite eliminar buena parte de él (si no todo).
Ejecutemos la aplicación (F5). El botón ocupa toda el área de trabajo (Figura 5a). Al pulsar el botón
aparece la ventana modal que bloquea el acceso a la ventana MainWindow (Figura 5b).
Maquetación con paneles eamos qué ocurre si tratamos de añadir elementos gráficos nuevos, como una etiqueta o una caja para
texto:
<Window x:Class="HolaMundo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid> <Label>Nombre:</Label> <TextBox></TextBox> <Button Click="Button_Click">Saluda</Button> </Grid> </Window>
En la ventana sólo vemos el último elemento. En realidad están
todos, pero uno encima del otro (véase la Figura 5). Es cosa del
elemento Grid, que dejamos para luego por ser complejo: si dos o
más elementos están en la misma celda de un Grid, se
superponen.
V
Figura 5. Los elementos se tapan
unos a otros en el Grid, por lo que
sólo se ve el botón, que está encima
del todo.
(a) (b)
Figura 4. (a) Ventana con el botón “Saluda”, que ocupa toda la superficie de la ventana. Ventana de diálogo
modal que aparece al ejecutar el método Button_Click.
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Maq
ueta
ció
n c
on
pan
ele
s
8
Empezamos por un elemento de maquetación más sencillo:
StackPanel. Un StackPanel apila vertical u horizontalmente sus
elementos.
<StackPanel> <Label>Nombre:</Label> <TextBox></TextBox> <Button Click="Button_Click"> Saluda </Button> </StackPanel>
La Figura 6 muestra el resultado de la nueva disposición de
elementos en el StackPanel: uno sobre el otro, por orden de
aparición en el fichero XAML.
Cada elemento WPF contiene decenas de atributos. Podemos
recurrir a un formulario para asignar valores distintos de los
“por defecto”, pero lo cierto es que a la larga resulta
conveniente usar el editor de XAML.
Hay más elementos de maquetación y se pueden incorporar
otros definidos por programadores. Los paneles que vienen de
serie son:
Grid: distribución de elementos en una tabla, con la
posibilidad de fundir filas y columnas.
StackPanel: distribución de elementos en sucesión
vertical u horizontal.
DockPanel: distribución de elementos con anclaje a
punto cardinal y posible expansión del último al área
sobrante.
WrapPanel: distribución de elementos en sucesión
vertical u horizontal en “líneas” (como el texto, que
fluye de una línea a la siguiente).
UniformGrid: distribución de elementos en una matriz
cuadrada.
Canvas: ubicación precisa de elementos.
(Algunos paneles definidos por programadores disponen
elementos gráficos de formas novedosas u ofrecen
animaciones para el desplazamiento o selección de sus
elementos. Se pueden encontrar ejemplos en
http://www.codeproject.com/Articles/37348/Creating-
Custom-Panels-In-WPF.aspx,
http://www.wpftutorial.net/CustomLayoutPanel.html o
http://www.codeproject.com/KB/WPF/Panels.aspx.)
Figura 6. Los tres elementos se muestran
uno sobre otro gracias al StackPanel.
Propiedades
Los elementos XAML tienen
numerosos atributos y al principio
cuesta un poco manejarse con
tantos. Puede venir bien invocar el
panel de edición de atributos. Con el
cursor en el elemento XAML cuyos
atributos se desea editar, aparece un
panel de propiedades al pulsar F4.
No obstante, a la larga es más
productivo usar el editor de XAML en
VS 2010, que asiste al programador
con los menús Intellisense.
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Maq
ueta
ció
n c
on
pan
ele
s
9
Las maquetas más complejas suelen formarse combinando diferentes paneles. En nuestro caso,
podemos crear un StackPanel vertical en el que apilar dos StackPanels adicionales, estos horizontales, y
el botón.
<Window x:Class="HolaMundo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <StackPanel> <StackPanel Orientation="Horizontal"> <Label>Nombre:</Label> <TextBox></TextBox> </StackPanel> <StackPanel Orientation="Horizontal"> <Label>Apellido:</Label> <TextBox></TextBox> </StackPanel> <Button Click="Button_Click">Saluda</Button> </StackPanel> </Window>
La ventana que hemos creado tiene espacio muerto bajo el botón “Saluda”. Esto es así porque hemos
creado la ventana con unas dimensiones fijas: 525 por 350 puntos. El problema es fácil de corregir:
<Window x:Class="HolaMundo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Width="525" SizeToContent="Height">
El atributo SizeToContent puede tomar los valores
Manual (que es el que toma por defecto), Height,
Width y WidthAndHeight. El efecto de seleccionar
Height para SizeToContent es una ventana con
anchura fija y altura ajustada al contenido de la
ventana (Figura 7a).
Hay un par de problemas adicionales. Por una
parte, los campos de texto son pequeños (aunque
crecen automáticamente conforme tecleamos
texto en ellos); por otra, el alineamiento de los
campos de texto no es perfecto y depende del
tamaño de las etiquetas (Figura 7b).
El elemento de maquetación Grid permite
solucionar los dos problemas. Un Grid consta de
una sección de declaración de filas y columnas. En
nuestro caso definiremos 3 filas y 2 columnas. La
primera fila contendrá la etiqueta “Nombre:” y su
campo de texto; la segunda, la etiqueta “Primer
apellido:” y su campo de texto; y la tercera, el
botón “Saluda”. Las etiquetas se dispondrán en la
Figura 8. Ventana con espacio indeseado.
(a)
(b)
Figura 7. (a) La ventana con altura ajustada a su
contenido. (b) Efecto de mal alineamiento cuando las
etiquetas tienen texto de diferentes longitudes.
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Maq
ueta
ció
n c
on
pan
ele
s
10
primera columna y los campos de texto en la segunda. El botón “Saluda” ocupará una columna que será
resultado de fundir las dos. La primera columna se ajustará al contenido y la segunda ocupará el resto
del espacio. Nuestra tabla presentará, pues, esta estructura:
Etiqueta Campo de texto ----------------------------------------------------------------------------------
Etiqueta Campo de texto ----------------------------------------------------------------------------------
--------------------------------------------------------- Botón ------------------------------------------------
El código XAML se complica un poco:
<Window x:Class="HolaMundo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Width="525" SizeToContent="Height"> <Grid> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Label Grid.Column="0" Grid.Row="0">Nombre:</Label> <TextBox Grid.Column="1" Grid.Row="0"></TextBox> <Label Grid.Column="0" Grid.Row="1">Primer apellido:</Label> <TextBox Grid.Column="1" Grid.Row="1"></TextBox> <Button Grid.Row="2" Grid.ColumnSpan="2" Click="Button_Click"> Saluda </Button> </Grid> </Window>
Por fin vemos lo conveniente de fijar atributos con la sintaxis Elemento.Atributo: no es apropiado definir
los atributos RowDefinitions y ColumnDefinitions con una simple cadena de texto, pues son en realidad
listas de elementos XML, algunos con sus propios atributos.
Hay un nuevo elemento sintáctico. Hay atributos (no elementos, como antes) con la sintaxis
Elemento.Atributo. Examinemos, por ejemplo, esta línea:
<Label Grid.Column="0" Grid.Row="1">Primer apellido:</Label>
El atributo Grid.Column permite asignar un valor a una propiedad Column definida en Grid, no en Label.
Los elementos de tipo Label no saben nada de los de tipo Grid y, aun así, pueden asociar un valor a una
propiedad de Grid. Los elementos WPF mantienen un diccionario que permite asociar valores a claves
(propiedades) de las que nada sabe. En este caso, esas propiedades permiten ubicar el elemento en una
fila/columna del Grid.
Si nosotros definiésemos un panel propio, digamos que con una clase ClockPanel, en el que hubiese
que ubicar los elementos alrededor de una circunferencia, por ejemplo, necesitaríamos que cada
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Co
nte
nid
o r
ico
, d
iseñ
o g
ráfi
co y
an
imaci
on
es
11
elemento especificase los grados en los que debe aparecer en la esfera del reloj. Podríamos, entonces,
usar una etiqueta como ésta:
<Label ClockPanel.Angle="90">Primer apellido:</Label>
Nótese que Label no sabe nada de ClockPanel (de hecho, ClockPanel es una invención nuestra y, de
momento, ni siquiera existe). Pese a ello, podemos gestionar atributos de este tipo, que reciben el
nombre de “propiedades pegadas” (attached properties).
Contenido rico, diseño gráfico y animaciones PF sigue un modelo de contenido rico. En muchos sistemas de construcción de aplicaciones con interfaz
gráfica de usuario hay serias limitaciones al contenido de los elementos. En algunas, los botones sólo
pueden contener, por ejemplo, texto y, opcionalmente, un icono. Escapar de esta restricción, cuando es
posible, obliga a construir nuevos elementos, lo que supone un incremento de complejidad enorme.
WPF, sin embargo, permite que muchos componentes contengan a otros componentes en su interior, lo
que facilita el diseño de aplicaciones con un acabado gráfico espectacular (si se trabaja codo con codo
con diseñadores gráficos, claro está).
Podemos probar a añadir un smiley al botón “Saluda”. Lo haremos con ayuda de Miorosoft Expression
Blend. Arrancamos Blend y desde su menú FileOpen Project/Solution… abrimos el proyecto VS 2010
HolaMundo y nos encontramos con la aplicación como se muestra en la Figura 9. La interfaz de Blend es
bastante compleja y no la analizaremos en detalle. Sólo queremos llevarnos una impresión acerca de su
uso y ver que es una herramienta especializada en adaptar la apariencia de nuestra aplicación, y no en
la lógica.
Podemos editar código XAML desde Microsoft
Expression Blend del mismo modo que hacemos
con VS 2010. Basta con pulsar el icono “<>” que
hay en la parte superior derecha del panel central
(el que contiene la ventana de nuestra aplicación).
Con el editor XAML de Microsoft Expression Blend
hemos escrito este texto en el fichero de texto
MainWindow.xaml, es decir, en el mismo fichero
que hemos estado editando en Visual Studio y
que forma parte del proyecto HolaMundo:
<Window x:Class="HolaMundo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Width="525" SizeToContent="Height"> <Grid> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/>
W
Figura 9. Microsoft Expression Blend 4 tras abrir el
proyecto HolaMundo.
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Co
nte
nid
o r
ico
, d
iseñ
o g
ráfi
co y
an
imaci
on
es
12
<ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Label Grid.Column="0" Grid.Row="0" Content="Nombre:"/> <TextBox Grid.Column="1" Grid.Row="0"/> <Label Grid.Column="0" Grid.Row="1" Content="Primer apellido:"/> <TextBox Grid.Column="1" Grid.Row="1"/> <Button Grid.Row="2" Grid.ColumnSpan="2" Click="Button_Click"> <StackPanel> <TextBlock TextAlignment="Center">Saluda</TextBlock> <Canvas Width="64" Height="64"> </Canvas> </StackPanel> </Button> </Grid> </Window>
El botón contiene ahora un StackPanel con dos elementos: un bloque de texto y un panel de tipo
Canvas de tamaño 64x64. Ahí dibujaremos el smiley. Con ayuda de la paleta de herramientas y el panel
de propiedades creamos el gráfico que se muestra en la Figura 10.
Blend permite crear efectos gráficos y animaciones. Vamos a
hacer que cuando el ratón entre en la región del botón el
smiley dé una vuelta.
Empezamos creando una animación. Seleccionamos el
Canvas y en el icono “+” del panel Objects and Timeline
seleccionamos New… y creamos una historia (storyboard) a
la que denominamos ZoomStoryboard. Con la marca de
tiempo (línea amarilla) en el instante 0, fijamos a 0 la propiedad Angle en el panel Transform del Canvas
que contiene al smiley, y fijamos a 360 su valor en el instante 1. Con eso conseguiremos que el smiley
dé una vuelta completa en un segundo.
Seleccionamos ahora el botón Saluda y seleccionamos el activo de tipo Behaviors denominado
ControlStoryBoardAction. En su panel, seleccionamos la historia ZoomStoryboard y el evento MouseEnter.
Podemos probar a ejecutar la aplicación y comprobar que cada vez que el cursor entra en el botón, se
ejecuta la animación. Bueno, no sólo entonces: también se dispara cuando cargamos la aplicación.
Luego eliminaremos este efecto indeseado (que aunque podemos eliminar desde Blend, eliminaremos
desde VS 2010).
Todo lo que hemos hecho con Blend se podría haber hecho directamente con VS 2010, pero hubiese
supuesto un esfuerzo considerablemente mayor, como comprobaremos en breve al analizar el XAML
generado.
Figura 10. Botón con dibujo creado con el
editor de Blend.
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Más
sob
re X
AM
L: re
curs
os,
dis
para
do
res,
tra
nsf
orm
aci
on
es
y r
efe
ren
cia
s
13
Más sobre XAML: recursos, disparadores, transformaciones y
referencias s hora de volver a VS 2010. Cerramos Blend y volvemos a VS 2010, que detectará que hubo cambios en
el proyecto y solicita, por tanto, recargarlo.
Analicemos el XAML que se ha generado desde Blend:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions" xmlns:ed="http://schemas.microsoft.com/expression/2010/drawing" x:Class="HolaMundo.MainWindow" Title="MainWindow" Width="525" SizeToContent="Height"> <Window.Resources> <Storyboard x:Key="ZoomStoryboard"> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[2].(RotateTransform.Angle)" Storyboard.TargetName="canvas"> <EasingDoubleKeyFrame KeyTime="0" Value="0"/> <EasingDoubleKeyFrame KeyTime="0:0:1" Value="360"/> </DoubleAnimationUsingKeyFrames> </Storyboard> </Window.Resources> <Window.Triggers> <EventTrigger RoutedEvent="FrameworkElement.Loaded"> <BeginStoryboard Storyboard="{StaticResource ZoomStoryboard}"/> </EventTrigger> </Window.Triggers> <Grid> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Label Grid.Column="0" Grid.Row="0" Content="Nombre:"/> <TextBox Grid.Column="1" Grid.Row="0"/> <Label Grid.Column="0" Grid.Row="1" Content="Primer apellido:"/> <TextBox Grid.Column="1" Grid.Row="1"/> <Button Grid.Row="2" Grid.ColumnSpan="2" Click="Button_Click"> <i:Interaction.Triggers> <i:EventTrigger EventName="MouseEnter"> <ei:ControlStoryboardAction Storyboard="{StaticResource ZoomStoryboard}"/> </i:EventTrigger> </i:Interaction.Triggers> <StackPanel> <TextBlock TextAlignment="Center"><Run Text="Saluda"/></TextBlock> <Canvas x:Name="canvas" Width="64" Height="64" RenderTransformOrigin="0.5,0.5"> <Canvas.RenderTransform> <TransformGroup> <ScaleTransform/> <SkewTransform/> <RotateTransform/> <TranslateTransform/>
E
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Más
sob
re X
AM
L: re
curs
os,
dis
para
do
res,
tra
nsf
orm
aci
on
es
y r
efe
ren
cia
s
14
</TransformGroup> </Canvas.RenderTransform> <Ellipse Fill="#FFFDFF00" Height="48" Canvas.Left="8" Stroke="Black" Canvas.Top="8" Width="48"/> <Ellipse Fill="#FFFF1700" Height="7.5" Canvas.Left="20" Stroke="Black" Canvas.Top="21.12" Width="7.5"/> <Ellipse Fill="#FFFF1700" Height="7.5" Canvas.Left="36" Stroke="Black" Canvas.Top="21.12" Width="7.5"/> <ed:Arc ArcThickness="0" ArcThicknessUnit="Pixel" EndAngle="-90" Fill="#FFF4F4F5" Height="12" Canvas.Left="20" Stretch="None" Stroke="Black" StartAngle="90" Canvas.Top="36.12" Width="23.5"/> </Canvas> </StackPanel> </Button> </Grid> </Window>
Complejo. Pero podemos analizar su contenido y comprobar que todo lo hecho con Blend, acaba
codificándose como texto en el fichero XAML.
Por una parte tenemos una sección de recursos, que es código XAML asignado a la propiedad Resources
de Window.
<Window.Resources> <Storyboard x:Key="ZoomStoryboard"> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[2].(RotateTransform.Angle)" Storyboard.TargetName="canvas"> <EasingDoubleKeyFrame KeyTime="0" Value="0"/> <EasingDoubleKeyFrame KeyTime="0:0:1" Value="360"/> </DoubleAnimationUsingKeyFrames> </Storyboard> </Window.Resources>
La sección de recursos es un diccionario que permite asociar elementos WPF a claves. La historia es que
hemos creado tiene por clave ZoomStoryboard y por valor una instancia de la clase
DoubleAnimationUsingKeyFrames. Las animaciones en WPF se forman con elementos simples. Una
DoubleAnimation, por ejemplo, es una animación consistente en el cambio de un valor de tipo de
double a lo largo del tiempo. Si vincuamos el valor de una propiedad de un elemento de la interfaz
gráfica a esa animación (como la escala, la altura, la opacidad…), su valor afectará al aspecto visual de
ese elemento. Una DoubleAnimationUsingKeyFrames es eso mismo, pero fijando tramas clave (key
frames) en las que el double debe tomar ciertos valores. En nuestro caso, en el instante 0 debe tener
valor 0 y en el instante 0:0:1 (un segundo después), debe valer 360. La animación tiene efecto sobre una
propiedad del Canvas en el que hemos puesto el smiley: el ángulo de rotación.
Después de la sección de recursos hay otra con disparadores (triggers). Los disparadores permiten
asociar acciones a eventos (entre otras cosas).
<Window.Triggers> <EventTrigger RoutedEvent="FrameworkElement.Loaded"> <BeginStoryboard Storyboard="{StaticResource ZoomStoryboard}"/> </EventTrigger> </Window.Triggers>
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Más
sob
re X
AM
L: re
curs
os,
dis
para
do
res,
tra
nsf
orm
aci
on
es
y r
efe
ren
cia
s
15
Este disparador es el responsable de que tan pronto se carga la ventana se inicie la animación
ZoomStoryboard. Los valores de atributos entre llaves son especiales. En este caso se indica que la
historia que debe ejecutarse se encuentra almacenada en el diccionario de recursos con la clave
ZoomStoryboard. Si eliminamos ese disparador (o, para el caso, su sección completa), eliminaremos la
animación indeseada.
Seguimos analizando el XAML. El botón tiene este código que fija el valor de algunas “propiedades
pegadas”:
<Button Grid.Row="2" Grid.ColumnSpan="2" Click="Button_Click"> <i:Interaction.Triggers> <i:EventTrigger EventName="MouseEnter"> <ei:ControlStoryboardAction Storyboard="{StaticResource ZoomStoryboard}"/> </i:EventTrigger> </i:Interaction.Triggers>
Los elementos en los espacios de nombres con sufijos i y ei son propios de Microsoft Expression Blend.
No entramos en detalles. Baste saber que los comportamientos (behaviors) que podemos fijar en Blend
se han incrustado en el XAML así.
El botón contiene un StackPanel y su primer elemento es un TextBlock, un bloque de texto. Su único
componente es una instancia de Run.
<TextBlock TextAlignment="Center"><Run Text="Saluda"/></TextBlock>
Hay varios tipos de elemento que podemos poner en un TextBlock y entre ellos destacamos Run (texto
normal), Italic (texto en cursiva) y Bold (texto en negrita). Pero no son los únicos. Y si ponemos texto a
pelo, WPF sabe que queríamos poner una marca Run y la pone por nosotros. Mucha de la
infraestructura de WPF nos permite eliminar algo de verbosidad en el código XAML (que aún así es muy
verboso).
Y llegamos por fin al Canvas:
<Canvas x:Name="canvas" Width="64" Height="64" RenderTransformOrigin="0.5,0.5"> <Canvas.RenderTransform> <TransformGroup> <ScaleTransform/> <SkewTransform/> <RotateTransform/> <TranslateTransform/> </TransformGroup> </Canvas.RenderTransform> <Ellipse Fill="#FFFDFF00" Height="48" Canvas.Left="8" Stroke="Black" Canvas.Top="8" Width="48"/> <Ellipse Fill="#FFFF1700" Height="7.5" Canvas.Left="20" Stroke="Black" Canvas.Top="21.12" Width="7.5"/> <Ellipse Fill="#FFFF1700" Height="7.5" Canvas.Left="36" Stroke="Black" Canvas.Top="21.12" Width="7.5"/> <ed:Arc ArcThickness="0" ArcThicknessUnit="Pixel" EndAngle="-90" Fill="#FFF4F4F5" Height="12" Canvas.Left="20" Stretch="None" Stroke="Black" StartAngle="90" Canvas.Top="36.12" Width="23.5"/> </Canvas>
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Acc
eso
a p
rop
ied
ad
es
desd
e c
ód
igo
tra
sero
16
Hay un atributo interesante con identificador x:Canvas. Permite asociar un nombre a un componente y
así poder referenciarlo desde otros puntos. De hecho, ya hemos referenciado a éste desde uno recurso:
<Window.Resources> <Storyboard x:Key="ZoomStoryboard"> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[2].(RotateTransform.Angle)" Storyboard.TargetName="canvas">
Así es como aplica la historia a un componente concreto: fijando como valor del objetivo el
identificador o nombre del elemento. En el Canvas hay un grupo de elementos asignados al atributo
RenderTransform: con componentes que aplican una transformación afín a un elemento en el momento
de visualizarse. La expresión
(UIElement.RenderTransform).(TransformGroup.Children)[2].(RotateTransform.Angle)
Estaba seleccionando el tercer componente (índice 2) del grupo de transformación, es decir, la rotación,
y centrando el interés en la propiedad Angle, que corresponde al ángulo. La rotación tiene lugar
centrada en el centro del Canvas gracias a este otro atributo:
<Canvas x:Name="canvas" Width="64" Height="64" RenderTransformOrigin="0.5,0.5">
El Canvas contiene tres elipses y un arco, elemento que forma parte de un espacio de nombres propio
de Microsoft Expression.
Cabe señalar otro aspecto interesante de los valores de ciertos atributos. En las elipses podemos ver
que hay varias formas de expresar un color:
<Ellipse Fill="#FFFDFF00" Height="48" Canvas.Left="8" Stroke="Black" Canvas.Top="8" Width="48"/>
Una forma es con #AARRGGBB (alfa, rojo, verde, azul) y otra con el propio nombre del color. WPF sabe
interpretar apropiadamente el valor que se desea usar a partir de una cadena. Para ello usa un rico
juego de conversores. Con unidades de medida, por ejemplo, sabe que “48” es media pulgada, que
también podríamos expresas con “0.5in”. Y “1cm” representa un centímeto, o lo que es lo mismo,
“10mm”.
¡Ah! Y fijémonos en los atributos Canvas.Left y Canvas.Top, que permite fijar las coordenadas X e Y de la
esquina superior izquierda del rectángulo de inclusión de la elipse en el Canvas que la contiene. Ya
vimos algo parecido con Grid.Column y Grid.Row.
Acceso a propiedades desde código trasero amos a acceder desde código trasero al valor que el usuario teclee en las cajas de texto. Para ello
necesitaremos poder acceder a las cajas de texto y deberán tener un identificador. En el código XAML
escribimos esto:
<Label Grid.Column="0" Grid.Row="0" Content="Nombre:"/> <TextBox x:Name="nombre" Grid.Column="1" Grid.Row="0"/> <Label Grid.Column="0" Grid.Row="1" Content="Primer apellido:"/> <TextBox x:Name="apellido" Grid.Column="1" Grid.Row="1"/>
V
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
En
lace
s
17
Volvamos al código trasero. En particular, al método que se invoca al pulsar el botón. Hagamos que el
método quede así:
private void Button_Click(object sender, RoutedEventArgs e) { MessageBox.Show("¡Hola, " + nombre.Text + " " + apellido.Text + "!"); }
Estamos accediendo a la propiedad Text de los elementos “nombre” y “apellido”. Decimos propiedad y
no campo porque Text no es un campo de tipo cadena, sino una “propiedad C#”. Las propiedades C#
son pares de métodos que permiten acceder o asignar un valor a, en principio, un campo. Lo cierto es
que detrás puede haber un campo o no haberlo, pero el programador que usa la propiedad tiene la
ilusión de que lo hay. En realidad se ejecutará código que podría calcular el valor que se quiere leer a
partir de uno o más campos (o de ninguno). El acceso a la propiedad Text no es una mera consulta a un
campo: probablemente consista en el acceso a un diccionario y en la aplicación de operaciones que nos
permitan ver el resultado como una cadena. Pero no hay de qué preocuparse: es todo tarea de WPF y
para nosotros se crea la ilusión de acceso a un simple campo.
Enlaces odemos enlazar diferentes elementos gráficos. Probemos a asociar el título de la ventana con el nombre
que se introduce en el formulario. Para eso hemos de asignar un nombre a la ventana y crear un enlace
(binding) que ligue su valor al del campo de texto que deseemos (y que ha de tener un nombre):
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" … Title="{Binding ElementName=nombre, Path=Text}" Width="525" SizeToContent="Height">
El enlace indica que hemos vincular el valor del campo Title de la ventana al del campo Text del
elemento llamado “nombre”. La sintaxis de las llaves puede reemplazarse por esta otra:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" … Width="525" SizeToContent="Height"> <Window.Title> <Binding ElementName="nombre" Path="Text"/> </Window.Title>
Al ejecutar la aplicación podemos
comprobar que título de ventana y
contenido de la caja de texto con el
nombre coinciden en todo
momento. Conforme tecleamos los
caracteres del nombre, el título de la
ventana se va actualizando.
P
Figura 11. El título de la ventana está vinculado al contenido de la
primera caja de texto.
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Vis
tas
y m
od
elo
s (o
casi
)
18
Vistas y modelos (o casi) o usual en una aplicación interactiva bien diseñada es que haya una clara separación entre la
representación de un objeto (la vista) y el propio objeto (el modelo). De hecho, lo ideal es que el
modelo dependa lo menos posible de la interfaz gráfica. Vamos a hacerlo ahora en nuestra aplicación,
aunque resultará un tanto impostado por ser ésta muy sencilla.
Creamos una nueva clase Persona. En el menú contextual del proyecto HolaMundo seleccionamos
AddClass… y creamos la clase Persona, que pasa a definirse así:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.ComponentModel; namespace HolaMundo { class Persona : INotifyPropertyChanged { private string _nombre; private string _apellido; public string NombreCompleto { get { return Nombre + " " + Apellido; } } public string Nombre { get { return _nombre; } set { if (value != _nombre) { _nombre = value; NotifyChange("Nombre"); NotifyChange("NombreCompleto"); } } } public string Apellido { get { return _apellido; } set { if (value != _apellido) { _apellido = value; NotifyChange("Apellido"); NotifyChange("NombreCompleto"); } } } void NotifyChange(string id)
L
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Vis
tas
y m
od
elo
s (o
casi
)
19
{ if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(id)); } #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; #endregion } }
La idea ahora es que el campo Nombre y el campo Apellido de una instancia de Persona estén siempre
sincronizados con las cajas de texto correspondientes en el formulario. Hemos creado cierta
infraestructura al efecto. Por una parte, Persona implementa la interfaz INotifyPropertyChanged. La
interfaz obliga a que se implemente un evento que se disparará cada vez que alguien modifique el valor
de una propiedad, avisando así a los suscriptores del evento. Nótese que el cambio del nombre o el
apellido no sólo cambia Nombre y Apellido, respectivamente: también cambia NombreCompleto.
Hemos de crear una instancia de Persona e indicar que el “contexto de datos” (DataContext) de la
ventana principal es esa instancia:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace HolaMundo { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { public readonly Persona LaPersona; public MainWindow() { InitializeComponent(); LaPersona = new Persona { Nombre = "Tu nombre", Apellido = "Tu apellido" }; DataContext = LaPersona; } private void Button_Click(object sender, RoutedEventArgs e)
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Pro
pie
dad
es
de d
ep
en
den
cia
20
{ MessageBox.Show("¡Hola, " + LaPersona.NombreCompleto + "!"); } } }
Y ahora, vinculemos el contenido de las cajas de texto con los de LaPersona. De paso, vincularemos el
título con el nombre completo:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions" xmlns:ed="http://schemas.microsoft.com/expression/2010/drawing" x:Class="HolaMundo.MainWindow" Width="525" SizeToContent="Height"> <Window.Title> <Binding Path="NombreCompleto"/> </Window.Title> …
<Label Grid.Column="0" Grid.Row="0" Content="Nombre:"/> <TextBox x:Name="nombre" Grid.Column="1" Grid.Row="0" Text="{Binding Nombre}"/> <Label Grid.Column="0" Grid.Row="1" Content="Primer apellido:"/> <TextBox x:Name="apellido" Grid.Column="1" Grid.Row="1" Text="{Binding Apellido}"/>
Y ya está. Nuestra ventana tiene por título el nombre completo y la instancia LaPersona siempre está
sincronizada con el formulario.
Propiedades de dependencia PF ofrece una herramienta muy interesante para crear propiedades que notifican automáticamente de
los cambios que experimentan: las propiedades de dependencia. De hecho, las propiedades de los
elementos WPF son realmente propiedades de dependencia. Estas propiedades no sólo notifican de los
cambios que experimentan: tienen valores por defecto, se pueden heredar sus valores en la jerarquía de
objetos, se pueden ligar a otras propiedades de dependencia, pueden usarse en animaciones y, lo que
quizá es más importante: no consumen memoria si no se les asigna un valor. Las propiedades de
dependencia se almacenan en un diccionario cuando se les asigna un valor. Si no lo tienen, WPF se
encarga de acceder al valor por defecto automáticamente. Se trata de un factor muy importante si
tenemos en cuenta que un elemento WPF puede tener más de medio centenar de propiedades.
Convirtamos nuestra persona en un objeto con propiedades de dependencia y, de momento, olvidemos
la sincronización del título de la ventana con el nombre completo. Más tarde recuperaremos esa
funcionalidad.
La nueva definición de Persona es ésta:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.ComponentModel; using System.Windows;
W
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Pro
pie
dad
es
de d
ep
en
den
cia
21
namespace HolaMundo { public class Persona : DependencyObject { public string NombreCompleto { get { return Nombre + " " + Apellido; } } public string Nombre { get { return (string)GetValue(NombreProperty); } set { SetValue(NombreProperty, value); } } public static readonly DependencyProperty NombreProperty = DependencyProperty.Register("Nombre", typeof(string), typeof(Persona), new UIPropertyMetadata("")); public string Apellido { get { return (string)GetValue(ApellidoProperty); } set { SetValue(ApellidoProperty, value); } } public static readonly DependencyProperty ApellidoProperty = DependencyProperty.Register("Apellido", typeof(string), typeof(Persona), new UIPropertyMetadata("")); } }
Complicado, ¿no? Afortunadamente VS 2010 nos ayuda con los denominados snippets, plantillas con
fragmentos de código fácilmente utilizables. Si tecleamos propdp (por “property: dependency property”),
el editor nos ofrece una plantilla como ésta:
public int MyProperty { get { return (int)GetValue(MyPropertyProperty); } set { SetValue(MyPropertyProperty, value); } } // Using a DependencyProperty as the backing store for MyProperty… public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register("MyProperty", typeof(int), typeof(ownerclass), new UIPropertyMetadata(0));
Con la ayuda del tabulador podemos asignar valor a los cuatro campos de la plantilla (que aparecen
con fondo verde en este documento).
La clase Persona hereda de DependencyObject, clase que ofrece la infraestructura necesaria para
soportar propiedades de dependencia. Una propiedad de dependencia es un objeto estático que se
registra en un diccionario con el método DependencyProperty.Register. Las instancias de un
DependencyObject pueden acceder al valor de su propiedad de dependencia con el método GetValue,y
asignarle un valor con SetValue (ambos heredados de DependencyObject). La propiedad C# que da
acceso a estos métodos nos facilita el acceso a su lógica.
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Un
a in
tro
du
cció
n a
l p
atr
ón
arq
uit
ect
ón
ico
MV
VM
22
No hemos tenido que notificar los cambios porque las propiedades de dependencia ya se ocupan de
ello automáticamente. Como NombreCompleto no es una propiedad de dependencia y ya no notifica de
los cambios, hemos perdido esa funcionalidad. No costaría nada recuperarla volviendo a implementar el
notificador de cambios en propiedades. Pero es mejor que pasemos a hablar de un patrón de diseño
importante en el mundo WPF: el patrón Modelo-Vista-Modelo de la Vista, o MVVM, por Model-View-
ViewModel.
Una introducción al patrón arquitectónico MVVM a separación entre vista y modelo que hemos hecho no es buena. Hemos acabado por tocar el modelo
para ajustarlo a la vista hasta el punto de construirlo con componentes de WPF, y eso es muy mala
práctica. Normalmente el modelo nos viene dado y tenemos poca capacidad de influencia sobre él.
Este problema es crucial en el diseño de aplicaciones
interactivas, salvo en las más triviales. Desde el inicio de la
programación de aplicaciones interactivas se plantearon la
cuestión de la separación de responsabilidades en este tipo
de sistemas. Un patrón exitoso es el conocido como
Modelo-Vista-Controlador o MVC, por Model-View-
Controller, que divide el sistema en tres componentes: el
modelo, la vista y el controlador. Podemos representar
gráficamente el concepto como se muestra en la Figura 12.
Un patrón de diseño, como MVC, no es una receta estructa
acerca de cómo implementar cierta funcionalidad, sino una
serie de criterios que deben considerarse al diseñar la
arquitectura de la aplicación e implementarla.
El usuario interactúa con dispositivos que “hablan” con un
controlador, el cual manipula un modelo (los datos) cuyo cambio fuerza la actualización de una vista,
que es lo que percibe el usuario. Hoy día el modelo presenta ciertas dificultades y se considera
superado por otros, como el denominado MVP, por Model-View-Presenter. También es un patrón con
tres componentes. En este caso se trata del modelo, la vista y el presentador. Vista y modelo parece
claro lo que son, pero ¿qué es el presentador? Es una capa entre la vista y el modelo. Cuando la vista
necesita algo del modelo, se lo solicita al presentador, que ofrece métodos que hacen cómodo para la
vista el acceso a los datos relevantes del modelo. Supongamos que el modelo tiene la fecha de
nacimiento de una persona, pero no la edad. El presentador podría ofrecer un método o propiedad
Edad que accediese a la fecha de nacimiento y al día actual para proporcionar el dato deseado. Y en
sentido inverso, cuando la vista necesita modificar el modelo, lo hace a través del presentador, que sabe
cómo “traducir” elementos de la vista en datos del modelo. También modelo y presentador interactúan:
el presentador lee y escribe sus datos y cuando el modelo cambia “espontáneamente” (es decir, por
eventos no relacionados con la interacción con el usuario), notifica al presentador de los cambios y éste
se encarga de actualizar la vista.
El patrón arquitectónico MVVM es una versión especializada de MVP para WPF. Establece mecanismos
propios de WPF para la comunicación Vista-Presentador (que aquí se denomina Modelo de la Vista). En
particular y limita las herramientas que podemos usar en cada capa.
L
Figura 12. Diagrama del patrón Modelo-
Vista-Controlador. (Imagen extraída de
Wikipedia.)
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Un
a in
tro
du
cció
n a
l p
atr
ón
arq
uit
ect
ón
ico
MV
VM
23
La vista es XAML 100% (o casi).
El modelo de la vista expone lógica vía órdenes (de las que nos ocupamos en breve), expone el
modelo mediante ligaduras entre propiedades y propiedades de dependencia y mantiene
información de estado de la interacción (pero sólo de la interacción).
El modelo mantiene los datos y sus operaciones, pero no lógica dependiente de cómo se usa.
(Si cambia espontáneamente, implementa un notificador de cambios en propiedades.)
Ahora vamos a seguir el patrón MVVM para que la aplicación vuelva a funcionar. Devolvamos el modelo
a una versión minimalista. El fichero Persona pasa a contener este texto:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace HolaMundo { public class Persona { public string Nombre { get; set; } public string Apellido { get; set; } public string NombreCompleto { get { return Nombre + " " + Apellido; } } } }
El modelo no “sabe” nada de WPF ni de cómo se usará en la aplicación. Se limita a mantener un par de
datos. Podría tener, además, operaciones para serializar el objeto, almacenarlo en disco, etcétera.
Preparemos una clase para el modelo de la vista: MainWindowViewModel. Recordemos que su papel es
hacer de puente entre la vista y el modelo.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.ComponentModel; namespace HolaMundo { public class MainWindowViewModel : INotifyPropertyChanged { public Persona Model; public string Nombre { get { return Model.Nombre;} set { Model.Nombre = value; NotifyChange("Nombre", "NombreCompleto"); } } public string Apellido { get { return Model.Apellido; } set { Model.Apellido = value; NotifyChange("Apellido", "NombreCompleto"); } }
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Un
a in
tro
du
cció
n a
l p
atr
ón
arq
uit
ect
ón
ico
MV
VM
24
public string NombreCompleto { get { return Model.Nombre + " " + Model.Apellido; } } void NotifyChange(params string[] ids) { if (PropertyChanged != null) foreach (var id in ids) PropertyChanged(this, new PropertyChangedEventArgs(id)); } #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; #endregion } }
Hemos de preocuparnos ahora de vincular Vista, Modelo y Modelo de la Vista. Lo haremos modificando
en App.xaml el arranque de la aplicación. Su contenido actual es éste:
<Application x:Class="HolaMundo.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="MainWindow.xaml"> <Application.Resources> </Application.Resources> </Application>
Y pasa a ser este otro:
<Application x:Class="HolaMundo.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Startup="Application_Startup"> <Application.Resources> </Application.Resources> </Application>
El método Application_Startup se definirá en App.xaml.cs así:
using System; using System.Collections.Generic; using System.Configuration; using System.Data; using System.Linq; using System.Windows; using System.Windows.Input; namespace HolaMundo { /// <summary> /// Interaction logic for App.xaml /// </summary>
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Órd
en
es:
el p
atr
ón
Co
mm
an
d
25
public partial class App : Application { private void Application_Startup(object sender, StartupEventArgs e) { var model = new Persona { Nombre = "Tu nombre", Apellido = "Tu apellido" }; var viewModel = new MainWindowViewModel {Model = model}; var view = new MainWindow {DataContext = viewModel}; view.Show(); } } }
Hemos creado un modelo, que se almacena en el modelo de la vista tan pronto se crea, y hemos creado
una vista cuyo contexto de datos es el modelo de la vista. La última acción consiste en mostrar la vista,
que es la ventana principal.
Para que la aplicación funciones hemos de eliminar las referencias a LaPersona o sus campos. Una de
ellas está en el método Button_Click, asociado al evento Click del Button. Vamos a deshacernos de los
eventos, pues no son recomendables en una aplicación MVVM.
Órdenes: el patrón Command l uso de eventos no es, en general, recomendable en aplicaciones de tamaño moderado o grande. Los
eventos crean referencias entre objetos que pueden prolongar la vida de éstos más allá de lo que el
programador supone. Es la causa principal de las fugas de memoria en aplicaciones .NET, por lo que
conviene tomar medidas profilácticas.
WPF soporta el patrón de diseño “orden” (command) que permite asociar lógica a acciones interactivas.
Y no sólo eso: permite también habilitar o deshabilitar la interacción de ciertos componentes en función
del estado de los datos.
En principio hemos de definir una clase que implemente la interfaz ICommand para cada orden del
sistema. Pero resulta más cómodo usar una clase única a la que suministrar, mediante delegados o
funciones anónimas, la lógica que deseamos. Esta clase suele denominarse RelayCommand o
DelegateCommand. Esta es nuestra versión:
public class RelayCommand : ICommand { #region Fields readonly Action<object> _execute; readonly Predicate<object> _canExecute; #endregion // Fields #region Constructors public RelayCommand(Action<object> execute) : this(execute, null) { }
E
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Órd
en
es:
el p
atr
ón
Co
mm
an
d
26
public RelayCommand(Action<object> execute, Predicate<object> canExecute) { if (execute == null) throw new ArgumentNullException("execute"); _execute = execute; _canExecute = canExecute; } #endregion #region ICommand Members [DebuggerStepThrough] public bool CanExecute(object parameter) { return _canExecute == null ? true : _canExecute(parameter); } public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } public void Execute(object parameter) { _execute(parameter); } #endregion }
Antes de estudiarla, veamos cómo usarla. Creemos una orden que se dispare cuando pulsamos el botón
“saluda”. El lugar natural para las órdenes es el modelo de la vista. Este es el código que le corresponde:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.ComponentModel; using System.Windows; using System.Windows.Input; namespace HolaMundo { public class MainWindowViewModel : INotifyPropertyChanged { public Persona Model; private readonly ICommand _saludaCommand; public ICommand SaludaCommand { get { return _saludaCommand; } } public MainWindowViewModel()
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Órd
en
es:
el p
atr
ón
Co
mm
an
d
27
{ _saludaCommand = new RelayCommand( o => MessageBox.Show("¡Hola, " + NombreCompleto + "!"), o => !string.IsNullOrEmpty(NombreCompleto.Trim())); } public string Nombre { get { return Model.Nombre;} set { Model.Nombre = value; NotifyChange("Nombre", "NombreCompleto"); } } public string Apellido { get { return Model.Apellido; } set { Model.Apellido = value; NotifyChange("Apellido", "NombreCompleto"); } } public string NombreCompleto { get { return Model.Nombre + " " + Model.Apellido; } } void NotifyChange(params string[] ids) { if (PropertyChanged != null) foreach (var id in ids) PropertyChanged(this, new PropertyChangedEventArgs(id)); } #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; #endregion } }
Al construir el RelayCommand proporcionamos dos funciones (anónimas en este caso): una que indica
qué hacer y la otra qué nos permite saber si la orden puede ejecutarse. En nuestro caso, el qué hacer es
lo de siempre: mostrar el diálogo modal. Y la orden podrá ejecutarse siempre que el nombre completo
contenga algún carácter no blanco.
El ICommand que se ejecutará al pulsar el botón se expone como propiedad de sólo lectura. Falta
vincular la orden a la pulsación del botón en MainWindow.xaml:
<Button Grid.Row="2" Grid.ColumnSpan="2" Command="{Binding SaludaCommand}">
Y ya está. Ejecutemos la aplicación y
comprobemos que el botón funciona.
Y comprobemos también que cuando
no hay texto en las cajas de texto no
podemos pulsar el botón, como se
muestra en la Figura 13.
Figura 13. El botón aparece deshabilitado porque no hay texto en
las cajas.
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Aú
n h
ay m
ás,
pero
no
para
ho
y
28
Podría pensarse que no hemos ganado mucho con las órdenes, pero el patrón orden aisla
efectivamente la lógica de la vista y la encapsula en una entidad que reúne la acción con un método
que permite saber cuándo es posible ejecutarla. Si ahora quisiésemos que otros elementos (opciones de
menú, gestos de teclados, etcétera) disparasen esa misma lógica, bastaría con asociarlos al mismo
RelayCommand. Todos esos elementos se activarían y desactivarían gracias al método CanExecute y
dispararían, cuando fuera posible, la misma acción vía Execute.
Aún hay más, pero no para hoy s imposible presentar todos los elementos que conforman WPF en una simple charla. He pretendido
mostrar algunos elementos básicos e introducir, aunque sea someramente, el patrón arquitectónico de
preferencia: MVVM. Entre lo que nos dejamos en el tintero:
Infinidad de controles.
Diferentes tipos de animaciones.
Estilos y plantillas que permiten personalizar prácticamente todo.
Gestión de colecciones que facilita la interacción con listas o árboles de datos.
La conexión a fuentes de datos provenientes de bases de datos o XML.
El modelo de navegación.
La versión empotrable en páginas web (Silverlight).
Los componentes multimedia.
Los efectos de bitmap.
Diseño de controles y paneles personalizados.
El trabajo con elementos 3D.
El diseño de pruebas unitarias para componentes MVVM.
Librerías de ayuda para el diseño de aplicaciones MVVM.
Uso de inyección de dependencias para facilitar la asociación entre elementos de la tríada
MVVM.
Extensiones para tinta, tacto, etcétera.
Conversores.
Eventos y órdenes enrutadas.
Espero haber despertado la curiosidad por WPF y C# para que acudáis ahora a las fuentes bibliográficas
o los blogs temáticos para aprender más.
E
Desarrollo de Aplicaciones con .NET y WPF Andrés Marzal
Fu
en
tes
bib
lio
grá
fica
s re
com
en
dab
les
29
Fuentes bibliográficas recomendables ólo queda recomendar algunos libros que permiten profundizar en WPF.
Windows Presentation Foundation Unleashed, de Adam Natham.
(Sacará una nueva edición en breve para cubrir WPF 4.0.)
Programming WPF, 2ª edición, de Chris Sells.
Essential Windows Presentation Foundations, de Chris Anderson.
Applications = Code + Markup: A Guide to the Microsoft Windows
Presentation Foundation, de Charles Petzold.
Hay también varios blogs recomendables. Entre ellos, os cito estos:
http://blogs.msdn.com/llobo/
http://sachabarber.net/
http://joshsmithonwpf.wordpress.com/
http://blogs.msdn.com/jgoldb/default.aspx
http://houseofbilz.com/Default.aspx
http://jesseliberty.com/
S
Recommended