Escolar Documentos
Profissional Documentos
Cultura Documentos
La plataforma .NET
Introducción
Introducción
La plataforma .NET es la propuesta de Microsoft para competir con la plataforma Java.
Mientras que Java se caracteriza por la máxima "write once, run anywhere", la plataforma
.NET de Microsoft está diseñada para que se puedan desarrollar componentes software
utilizando casi cualquier lenguaje de programación, de forma que lo que escribamos en un
lenguaje pueda utilizarse desde cualquier otro de la manera más transparente posible
(utilizando servicios web como middleware). Esto es, en vez de estar limitados a un único
lenguaje de programación, permitimos cualquier lenguaje de programación, siempre y cuando
se adhiera a unas normas comunes establecidas para la plataforma .NET en su conjunto. De
hecho, existen compiladores de múltiples lenguajes para la plataforma .NET: Visual Basic .NET,
C#, Managed C++, Oberon, Component Pascal, Eiffel, Smalltalk, Cobol, Fortran, Scheme,
Mercury, Mondrian/Haskell, Perl, Python, SML.NET...
La plataforma .NET apuesta por un futuro en el que las aplicaciones se ejecutan de manera
distribuida en Internet. Así, una aplicación se ejecuta en un solo servidor y no existen
múltiples copias de la misma. Además, una misma aplicación puede "adornarse" con distintas
interfaces para que, desde diferentes dispositivos (teléfonos móviles, PDAs, portátiles, etc.)
pueda accederse a la misma. La plataforma .NET no es más que un conjunto de tecnologías
para desarrollar y utilizar componentes que nos permitan crear formularios web, servicios web
y aplicaciones Windows.
Para crear aplicaciones para la plataforma .NET, tanto servicios Web como aplicaciones
tradicionales (aplicaciones de consola, aplicaciones de ventanas, servicios de Windows NT,
etc.), Microsoft ha publicado el denominado kit de desarrollo de software conocido como .NET
Framework. Contiene el CLR (Common Languaje Runtime), el .NET Framework Clases y
características avanzadas como ADO.NET (para acceso a bases de datos), ASP.NET (para
generar páginas activas) y WinForms (para construir aplicaciones Windows). Adicionalmente
puede emplearse Visual Studio.NET que permite hacer todo la anterior desde una interfaz
visual basada en ventanas. Ambas herramientas pueden descargarse gratuitamente desde
http://www.msdn.microsoft.com/net.
Este recolector es una aplicación que se activa cuando se quiere crear algún
objeto nuevo y se detecta que no queda memoria libre para hacerlo.
Entonces el recolector recorre la memoria dinámica asociada a la aplicación,
detecta qué objetos hay en ella que no puedan ser accedidos por el código
de la aplicación, y los elimina para limpiar la memoria de "objetos basura" y
permitir la creación de otros nuevos. Gracias a este recolector se evitan
errores de programación muy comunes como intentos de borrado de
objetos ya borrados, agotamiento de memoria por olvido de eliminación de
objetos inútiles o solicitud de acceso a miembros de objetos ya destruidos.
Como se puede deducir de las características comentadas, el CLR lo que hace es gestionar la
ejecución de las aplicaciones diseñadas para la plataforma .NET. Por esta razón, al código de
estas aplicaciones se le suele llamar código gestionado, y al código no escrito para ser
ejecutado directamente en la plataforma .NET se le suele llamar código no gestionado.
Assemblies
En la plataforma .NET, una aplicación está formada por uno o varios assemblies. Al poder
coexistir distintas versiones de un assembly, se eliminan muchos de los problemas que
caracterizan a las aplicaciones típicas de Windows, facilitando el despliegue, actualización y
eliminación de aplicaciones. De hecho, una aplicación concreta podría utilizar simultáneamente
varias versiones de un assembly.
Biblioteca de clases .NET
ADO.NET
Los formularios Windows están construidos sobre la base de la plataforma .NET y permiten
construir complejas aplicaciones Windows en un entorno de desarrollo visual de aplicaciones
(RAD: Rapid Application Development), tal como hasta ahora se venía haciendo con lenguajes
del estilo de Visual Basic o Delphi.
Formularios web
Los formularios web, que se construyen con ASP.NET, constituyen la evolución natural y lógica
de ASP. Siguiendo el mismo estilo que su antecesor (editar una página y listo), ASP.NET
permite utilizar controles complejos, facilita la gestión de sesiones, permite separar la interfaz
de la lógica interna, elimina la distinción entre ASP e ISAPI y nos permite emplear cualquier
lenguaje de programación que esté soportado por la plataforma .NET.
Soporte para múltiples lenguajes
Para permitir el desarrollo de componentes utilizando múltiples lenguajes de programación, la
plataforma .NET establece un sistema de tipos común (CTS: Common Type System) y una
especificación que permite que puedan interactuar fragmentos de código escritos en distintos
lenguajes (CLS: Common Language Specification).
La plataforma .NET permite utilizar una amplia gama de lenguajes de programación, como es
el caso de
Ejecución de código
Para que un lenguaje de programación sea soportado por la plataforma .NET, ha de existir un
compilador que traduzca de este lenguaje a MSIL ("managed code"). A la hora de ejecutar el
código intermedio, éste es siempre compilado a código nativo.
El SDK para la plataforma .NET
Microsoft pone a disposición de todo aquél que esté interesado el kit de desarrollo de software
para la plataforma .NET: The Microsoft .NET Framework Software Development Kit (SDK). Este
kit incluye la plataforma .NET y todo lo necesario para desarrollar, compilar, probar y distribuir
aplicaciones para la plataforma .NET (así como documentación, ejemplos, herramientas en
línea de comandos y compiladores).
Para utilizar el kit de desarrollo de software se necesita tener uno de los siguientes sistemas
operativos:
y también se recomienda tener instalado el navegador web Microsoft Internet Explorer 5.01 o
posterior.
El Visual Studio .NET ya incluye la plataforma .NET, por lo que, si ya tiene instalado el Visual
Studio .NET no tendrá que instalar el SDK por separado.
Material complementario
El mismísimo Bill Gates se jacta de que la plataforma .NET es el producto software sobre el
que más se ha escrito y al que se le han otorgado más premios antes incluso de que existiese
realmente (Software Development, mayo 2002, página 17: "3.5 millones de usuarios beta, 200
libros y 764 grupos de usuarios"), por lo que no le será difícil al lector encontrar material más
que de sobra para pasarse leyendo el resto de su vida (y alguna de las siguientes). Dada la
gran abundancia de material disponible, se recomienda al lector que comience a trabajar con
la plataforma .NET lo antes posible y recurra al material complementario únicamente cuando
tenga que resolver alguna duda concreta.
Enlaces
http://www.microsoft.com/net/
http://msdn.microsoft.com/net/
http://www.gotdotnet.com
nntp://msnews.microsoft.com
Picking a Winner: .NET vs. J2EE, Jim Farley, Software Development, March
2001
.NET J2EE
Lenguaje de
C#... Java
programación
Plataforma Windows ...
CLR [Common JVM [Java Virtual Machine] /
Máquina virtual Language JRE [Java Runtime
Runtime] Environment]
Lenguaje
MSIL Java bytecodes
interpretado
Clientes Windows .NET Forms Swing
Clientes Web ASP.NET JSP / Servlets
Servidores de
??? EJB [Enterprise Java Beans]
aplicaciones
Acceso a bases de
ADO.NET JDBC / EJB-SQL
datos
JMS [Java Messaging Service]
Paso de mensajes MSMQ
/ Msg EJBs
Integración con JCA [Java Connector
COM TI
sistemas previos Architecture]
Visual Studio .NET
El entorno de desarrollo
Al arrancar por primera vez Visual Studio 2005 nos aparece la siguiente ventana:
Nuestro interés es trabajar en C#, así que seleccionaremos esa configuración predeterminada:
Configuración
Manteniéndose al día...
Proyectos recientes
Esta sección proporciona un rápido acceso a los proyectos recientes con los que hayamos
estado trabajando, así como acceso a sitios web (accesibles por FTP, HTTP, IIS) o al sistema
de archivos local.
Explorador de Soluciones
La ventana "Vista de Clases" muestra las clases de un proyecto, sus miembros y su estructura
jerárquica de herencia. Esta vista de un proyecto se va actualizando conforme escribimos
código.
Lista de Tareas
La lista de tareas es el sitio donde podemos ir anotando las cosas que tengamos pendientes
(idetificadas por el token "TODO"). Cuando empleamos asistentes ("wizards"), éstos se
encargan de recordarnos lo que tengamos pendiente añadiendo tareas a esta lista. Por otro
lado, los errores y warnings del compilador también aparecerán en esta lista.
Si una tarea de la lista de tareas está asociada a algún fichero, haciendo doble click sobre ella
pasamos de forma instantánea a la posición correspondiente del fichero asociado.
Examinador de objetos
Regiones
En Visual Studio se pueden definir regiones de código fácilmente para que luego resulte más
sencillo analizar su funcionamiento. Aparte de las funciones (que podemos expandir y
contraer), también podemos especificar cómo agrupar fragmentos de nuestro código mediante
la directiva #region.
Una vez que hayamos diseñado nuestras macros, podemos asociarlas a combinaciones de
teclas ("Herramientas | Opciones | Entorno | Teclado") e incluso añadirlas a los menús del
Visual Studio o ponerlas en las barras de botones del IDE.
Ayuda
Como no podía ser menos, el VS.NET incluye un potente sistema de ayuda que pone a nuestra
disposición ayuda en función del contexto en el que nos encontremos y las acciones que
realicemos con el ratón y el teclado (ventana "Ayuda Dinámica"):
Como es habitual en cualquier aplicación Windows, podemos acceder en todo momento a la
ayuda si utilizamos la tecla F1. Como particularidad del VS.NET, lo que nos muestra la ayuda
viene filtrado por la configuración que hayamos seleccionado para nuestro entorno.
Enlaces de interés
http://msdn.microsoft.com/vstudio/
http://code.msdn.microsoft.com/
El lenguaje de programación C#
C# (leído en inglés "C Sharp" y en español "C Almohadilla") es el nuevo lenguaje de propósito
general diseñado por Microsoft para su plataforma .NET.
Aunque es posible escribir código para la plataforma .NET en muchos otros lenguajes, C# es el
único que ha sido diseñado específicamente para ser utilizado en ella, por lo que programarla
usando C# es mucho más sencillo e intuitivo que hacerlo con cualquiera de los otros lenguajes
ya que C# carece de elementos heredados innecesarios en .NET. Por esta razón, se suele decir
que C# es el lenguaje nativo de .NET
Es costumbre desde la época dorada del lenguaje C (quizá una de las pocas costumbres que se
mantienen en el mundo de la informática) que se presente un lenguaje de programación
empleando un programa que muestra en la consola el mensaje ¡Hola, mundo! (para ser más
precisos deberíamos decir Hello, world!). Sigamos manteniendo esta costumbre:
¡Hola, mundo!
/*
Fichero: Saludo.cs
Fecha: Enero de 2004
Autores: F. Berzal, F.J. Cortijo y J.C.Cubero
Comentarios:
Primer programa escrito en C#
*/
using System;
Console.WriteLine("¡Hola, mundo!");
Console.ReadLine(); // Enter para terminar.
}
}
Lo primero que hay que resaltar es que C# es un lenguaje sensible a las mayúsculas, por lo
que, por ejemplo, Main es diferente a main, por lo que deberá prestar atención a la hora de
escribir el código ya que la confusión entre mayúsculas y minúsculas provocará errores de
compilación.
Todas las órdenes acaban con el símbolo del punto y coma ( ;). Los bloques de órdenes (parte
iterativa de un ciclo, partes dependientes de una instrucción condicional -parte if y parte
else-, código de un método, etc.) se encierran entre llaves { } y no se escribe el ; después
de la llave de cierre (observar en el ejemplo el método Main).
Si el lector ha programado en C++ no habrá tenido dificultad en localizar los comentarios que
hemos insertado en el programa ya que la sintaxis es idéntica en ambos lenguajes: existen
comentarios de línea, cuyo comienzo se especifica con los caracteres // y comentarios de
formato libre, delimitados por /* y */.
La línea using System declara que se va a usar el espacio de nombres (namespace) llamado
System. Esta declaración no es igual a un #include de C#, tan solo evita escribir el prefijo
System cada vez que se use un elemento de ese espacio de nombres. Por ejemplo, Console
es un objeto del espacio de nombres System; en lugar de escribir su nombre completo
(System.Console) podemos escribir solamente Console al haber declarado que se va a
emplear el espacio de nombres System.
En cuanto a las instrucciones que efectivamente se ejecutan, el método Main() llama a los
métodos WriteLine() y ReadLine() del objeto Console. El primero se encargan de
mostrar una cadena en la consola (Símbolo de Sistema, en Windows XP) y el segundo de
tomar una cadena de la consola (teclado). Aunque esta última instrucción pueda parecer
innecesaria, de no escribirla se mostraría la cadena ¡Hola, mundo! y se cerraría
inmediatamente la consola, por lo que no podríamos contemplar el resultado de la ejecución.
La última instrucción detiene la ejecución del programa hasta que se introduce una cadena
(pulsar ENTER es suficiente) y a continuación termina la ejecución del programa y se cierra la
consola. Así, cuando usemos aplicaciones de consola siempre terminaremos con esta
instrucción.
Comentarios
Los comentarios tienen como finalidad ayudar a comprender el código fuente y están
destinados, por lo tanto, a los programadores. No tienen efecto sobre el código ejecutable ya
que su contenido es ignorado por el compilador (no se procesa). La sintaxis de los comentarios
en C# es idéntica a la de C++ y se distinguen dos tipos de comentarios:
/*
En múltiples líneas, como se viene
haciendo desde "los tiempos de C"
*/
Identificadores
Deben comenzar por una letra letra o con el carácter de subrayado ( _), que
está permitido como carácter inicial (como era tradicional en el lenguaje C).
No pueden contener espacios en blanco.
Pueden contener caracteres Unicode, en particular secuencias de escape
Unicode.
Son sensibles a mayúsculas/minúsculas.
No pueden coincidir con palabras reservadas (a no ser que tengan el prefijo @
que habilita el uso de palabras clave como identificadores).
Palabras reservadas
abstract, as, base, bool, break, byte, case, catch, char, checked, class, const,
continue, decimal, default, delegate, do, double, else, enum, event, explicit,
extern, false, finally, fixed, float, for, foreach, goto, if, implicit, in, int,
interface, internal, is, lock, long, namespace, new, null, object, operator, out,
override, params, private, protected, public, readonly, ref, return, sbyte,
sealed, short, sizeof, stackalloc, static, string, struct, switch, this, throw,
true, try, typeof, uint, ulong, unchecked, unsafe, ushort, using, virtual, void,
volatile, while
Literales
Un literal es una representación en código fuente de un valor. Todo literal tiene asociado un
tipo, que puede ser explícito (si se indica en el literal, mediante algún sufijo, por ejemplo) o
implícito (se asume uno por defecto).
Literales enteros.
Permiten escribir valores de los tipos enteros: int, uint, long y ulong.
Los literales enteros tienen dos formatos posibles: decimal y hexadecimal.
Los literales hexadecimales tienen el sufijo 0x.
Literales enteros
123 // int
0x7B // hexadecimal
123U // unsigned
123ul // unsigned long
123L // long
Literales reales.
Los literales reales permiten escribir valores de los tipos float, double y
decimal.
Hay que tener en cuenta que, en un literal real, siempre son necesarios
dígitos decimales tras el punto decimal. Por ejemplo, 3.1F es un literal
real, pero no así 1.F.
Literales reales
Literales de caracteres.
Un literal de caracteres representa un carácter único y normalmente está
compuesto por un carácter entre comillas simples, por ejemplo 'A'.
Literales de caracteres
Literales de cadena.
Literal null.
Órdenes
Estas aplicaciones emplean la consola para representar las secuencias de entrada, salida (y
error) estándar.
Una aplicación de consola se crea en Visual Studio .NET seleccionando Archivo, Nuevo y
Proyecto. Cuando aparece la ventana Nuevo proyecto se selecciona Proyectos de
Visual C# y Aplicación de consola:
E/S simple
using System;
class PideNombre
{
static void Main(string[] args)
{
Console.Write ("Introduzca su nombre: "); // 1
string nombre = Console.ReadLine(); // 2
pero no avanza a la siguiente línea de la consola, por lo que cuando se ejecuta la instrucción 2
lo que escribe el usuario se muestra a continuación, en la misma línea. La cadena que escribe
el usuario se guarda en la variable nombre y se elimina del buffer de entrada el terminador de
línea. Cuando se valida la entrada (al pulsar ENTER) se avanza a la siguiente línea. La
instrucción 3 muestra una cadena, resultado de concatenar un literal y la cadena introducida
por el usuario. Finalmente, la instrucción 4 es necesaria para detener la ejecución del
programa (realmente, la finalización del mismo) hasta que el usuario pulse ENTER. Observar
que aunque el método Readline devuelva una cadena, éste valor devuelto no es usado. En la
siguiente figura mostramos dos ejemplos de ejecución.
Aplicaciones Windows
Una aplicación basada en ventanas (aplicación Windows, en lo que sigue) utilizan ventanas y
componentes específicos para interactuar con el usuario. Las peticiones de datos se realizan
con componentes de entrada de texto (por ejemplo, con un TextBox) o mediante la selección
en una lista de posibilidades (por ejemplo, con un ComboBox). Las salidas pueden realizarse
de múltiples maneras, empleando componentes Label, ventanas de mensajes MessageBox,
etc.
Por ejemplo, en la figura siguiente mostramos una aplicación que responde mostrando una
ventana de mensaje (MessageBox) cuando se pincha sobre el botón titulado Saludo. Basta
con ejecutar este código cada vez que se pinche en dicho botón:
Una aplicación más compleja podría pedir el nombre del usuario en un componente TextBox y
mostrarlo empleando un componente Label cuando se pincha en el botón titulado Saludo:
Una aplicación de ventanas se crea fácilmente en Visual Studio .NET seleccionando Archivo,
Nuevo y Proyecto. En la ventana Nuevo proyecto se selecciona ahora Proyectos de
Visual C# y Aplicación para
Windows.
Tipos de datos
Los tipos de datos ofrecidos por C# al programador forman parte de un sistema unificado en el
que todos los tipos de datos (incluidos los definidos por el usuario) derivan, aunque sea de
manera implícita, de la clase System.Object. Por herencia dispondrán de todos los miembros
definidos en ésta clase, en particular los útiles métodos Equals(), GetHashCode(),
GetType() y ToString() que describiremnos más adelante.
tipos valor
tipos referencia
Tipos valor
Los tipos básicos son tipos valor. Si una variable es de un tipo valor contiene únicamente un
valor del tipo del que se ha declarado.
Los tipos predefinidos de C# son tipos disponibles en la plataforma .NET y que, por
comodidad, en C# se emplean usando un alias. En la tabla siguiente enumeramos los tipos
simples detallando su nombre completo, su alias, una breve descripción, el número de bytes
que ocupan y el rango de valores.
Tipos referencia
C# proporciona dos tipos referencia predefinidos: object y string. Todos los demás tipos
predefinidos son tipos valor.
El tipo object es el tipo base del cual derivan todos los tipos básicos predefinidos y los
creados por el usuario. Pueden crearse nuevos tipos referencia usando declaraciones de clases
(class), interfaces (interface) y delegados (delegate), y nuevos tipos valor usando
estructuras struct.
Los objetos de las clases creadas por el usuario son siempre de tipo referencia. El operador
new permite la creación de instancias de clases definidas por el usuario. new es muy diferente
en C# y en C++:
Considere el siguiente fragmento de código, en el que todas las variables son del mismo tipo:
ObjetoDemo).
Tipos referencia
class ObjetoDemo
{
public int Valor;
}
class AppDemoRef
{
Console.ReadLine();
}
El tipo string es un tipo especial de tipo referencia. De hecho, parece más un tipo valor ante
la asignación. Observe el ejemplo:
string s1 = "Hola";
string s2 = s1;
En este punto s2 referencia al mismo objeto que s1. Sin embargo, cuando el valor de s1 es
modificado, por ejemplo con:
s1 = "Adios";
lo que ocurre es que se crea un nuevo objeto string referenciado por s1. De esta forma, s1
contiene "Adios" mientras que s2 mantiene el valor "Hola". Esto es así porque los objetos
string son immutables, por lo que, para cambiar lo que referencia una variable string
debe crearse un nuevo objeto string.
Variables y constantes
Variables
Nombres de variables
Los nombres que pueden asignarse a las variables deben regirse por unas normas básicas:
Finalmente, recordar que, como identificador que es, el nombre de una variable es sensible a
las mayúsculas y no pueden coincidir con una palabra reservada a no ser que tenga el prefijo
@, aunque no es una práctica recomendada.
Declaración de variables
Antes de usar una variable se debe declarar. La declaración de una variable indica al
compilador el nombre de la variable y su tipo. Una declaración permite que se pueda reservar
memoria para esa variable y restringir el espacio (cantidad de memoria) que requiere, los
valores que pueden asignarsele y las operaciones en las que puede intervenir.
La sintaxis de una declaración es sencilla: tan sólo hay que especificar el tipo de la variable y
el nombre que se le asocia. La declaración debe concluir con el carácter punto y coma. Por
ejemplo, si vamos a emplear una variable para guardar en ella el valor del área de un círculo
debemos:
float Area;
Cuando se van a declarar múltiples variables del mismo tipo no es necesario que cada
declaración se haga por separado, pueden agruparse en la misma línea compartiendo el tipo.
Por ejemplo, las declaraciones:
float Radio;
float Area;
Acceso a variables
Una variable se usa para asignarle un valor (acceso de escritura) o para utilizar el valor
almacenado (acceso de lectura).
Una vez declarada una variable debe recibir algún valor (es su misión, después de todo). Este
valor lo puede recibir de algún dispositivo (flujo de entrada) o como resultado de evaluar una
expresión. La manera más simple de proporcionar un valor a una variable es emplear la
instrucción de asignación:
Radio = 10.0F;
En el ejemplo se asigna el valor (literal entero) 10 a la variable Radio. El valor que tuviera
almacenado la variable Radio se pierde, quedando fijado a 10.
La manera más simple de leer el valor de una variable es emplearla como parte de una
expresión, por ejemplo, en la parte derecha de una instrucción de asignación:
La variable Radio (su valor) se emplea en la expresión 2 * 3.1416 * Radio * Radio para
calcular el área de un círculo de radio Radio. Una vez calculado el valor de la expresión, éste
se asigna a la variable Area.
Otra manera de acceder al valor de una variable para lectura es emplearla como el argumento
de una instrucción de escritura WriteLine. Por ejemplo,
Console.WriteLine(Area);
mostrará en la consola el valor de la variable Area.
using System;
class Area1
{
static void Main(string[] args)
{
float Radio = 10.0F;
float Area = 2 * 3.1416F * Radio * Radio;
Console.WriteLine(Area);
Console.ReadLine();
}
}
Como puede parecer evidente, emplear una variable que no ha sido declarada produce un
error en tiempo de compilación. En C#, además, hay que asignarle un valor antes de utilizarla.
Si no se hace se genera un error en tiempo de compilación ya que esta comprobación (igual
que ocurre en Java) se efectúa por el compilador.
void f()
{
int i;
Console.WriteLine(i); // Error: uso de la variable local
no asignada 'i'
}
Constantes
Una constante se define de manera parecida a una variable: modeliza una zona de memoria
que puede almacenar un valor de un tipo determinado. La diferencia reside en que esa zona de
memoria (su contenido) no puede modificarse en la ejecución del programa. El valor de la
constante se asigna en la declaración.
Solo se puede consultar el valor de una constante, nunca se debe intentar modificarlo porque
se produciría un error en tiempo de compilación. Por ejemplo, la siguiente instrucción:
Area = 2 * PI * Radio * Radio;
utiliza la constante PI declarada anteriormente, usando su valor para evaluar una expresión.
using System;
class Area2
{
static void Main(string[] args)
{
const float PI = 3.1416F;
float Radio = 10.0F;
float Area = 2 * PI * Radio * Radio;
Console.WriteLine(Area);
Console.ReadLine();
}
}
El ámbito (del inglés scope) de una variable y/o constante indica en qué partes del código es
lícito su uso.
En el ámbito de una variable no puede declararse otra variable con el mismo nombre (aunque
sea en un bloque interno, algo que si está permitido en C++):
Categorías Operadores
Aritméticos + - * / %
Lógicos (booleanos y bit a bit) & | ^ ! ~ && ||
Concatenación de cadenas +
Incremento y decremento ++ --
Desplazamiento << >>
Relacionales == != < > <= >=
= += -= *= /= %= &= |= ^=
Asignación
<<= >>=
Acceso a miembros .
Acceso por índices []
Conversión de tipos explícita ()
Conditional ? :
Creación de objetos new
Información de tipos as is sizeof typeof
Control de excepciones de
checked unchecked
desbordamiento
Direccionamiento indirecto y
* -> [] &
dirección
Son operadores binarios y se colocan entre los argumentos sobre los que se
aplican, proporcionando un resultado numérico (Por ejemplo, 7+3.5, 66 %
4).
a += 22;
equivale a
a = a + 22;
using System;
class PruebaDesbordamiento
{
static short x = 32767; // Maximo valor short
static short y = 32767;
El resultado es:
Valor -unchecked-: -2
Valor por defecto: -2
Si añadimos la función:
y la llamada:
El operador as
El operador as se utiliza para realizar conversiones entre tipos compatibles (al estilo de los
casts dinámicos de C++). El operador as se utiliza en expresiones de la forma:
<expresion> as type
using System;
class Clase1 {}
class Clase2 {}
El resultado es:
0:no es string
1:no es string
2:'Hola'
3:no es string
4:no es string
5:no es string
if (c != null) c.Drive();
}
El operador is
<expresion> is type
El siguiente ejemplo realiza la misma tarea que comentamos en el ejemplo sobre el operador
as solo que ahora no hay ningún objeto local con referencia explícita sobre el que se ejecute el
método Drive.
El operador typeof
typeof (tipo)
De esta forma se puede hacer reflexión para obtener dinámicamente información sobre los
tipos (como en Java).
...
Console.WriteLine(typeof(int).FullName);
Console.WriteLine(typeof(System.Int).Name);
Console.WriteLine(typeof(float).Module);
Console.WriteLine(typeof(double).IsPublic);
Console.WriteLine(typeof(Point).MemberType);
...
Un ejemplo en el que se emplean los operadores is, as y typeof para discriminar el tipo del
argumento recibido en una función polimórfica y trabajar con él es el siguiente:
using System;
class Clase1 {}
class Clase2 {}
Type t;
if (o is Clase1)
{
Console.WriteLine ("\no es de Clase1");
a = (Clase1)o;
Console.WriteLine ("--> {0}", typeof(Clase1).FullName);
t = typeof(Clase1);
}
else if (o is Clase2)
{
Console.WriteLine ("\no es de Clase2");
b = o as Clase2;
t = typeof(Clase2);
}
else
{
Console.WriteLine ("\no no es ni de Clase1 ni de Clase2.");
t = o.GetType();
}
Console.WriteLine ("Su tipo es " + t.FullName);
}
Console.ReadLine();
}
}
El resultado es:
o es de Clase1
--> Clase1
Su tipo es Clase1
o es de Clase2
Su tipo es Clase2
o no es ni de Clase1 ni de Clase2.
Su tipo es System.String
Precedencia
También podrían presentarse por precedencia. En la tabla siguiente los enumeramos de mayor
a menor precedencia:
Categorías Operadores
Paréntesis: (x)
Post-incremento: x++
Valor positivo: +
Valor negative: -
No: !
Pre-incremento: ++x
Post-decremento: --x
Multiplicación: *
División: /
Multiplicativos
Resto: %
Suma: +
Aditivos
Resta: -
Desplazamiento de bits a la izquierda: <<
Desplazamiento
Desplazamiento de bits a la derecha: >>
Menor: <
Mayor: >
Conversión de tipo: as
Igualdad ==
Desigualdad !=
Bitwise AND &
Bitwise XOR ^
Bitwise OR |
Logical AND &&
Logical OR ||
Condicional ternario ?:
Asignación =, *=, /=, %=, +=, -=, <<=, >>=, &=, ^=, |=
Asociatividad
x = y = z se evalúa como x = (y = z)
x + y + z se evalúa como (x + y) + z
Estructuras de control
Las estructuras de control de C# son similares a las de C y C++. La diferencia más notable
radica en que la instrucción condicional if y los ciclos while y do están controlados por una
expresión lógica (tipo Boolean).
Esta restricción hace que las instrucciones sean más seguras al evitar posibles fuentes de
error, o al menos, facilitan la legibilidad del código. Por ejemplo, en la siguiente instrucción
(válida en C++):
if (a)
la expresión a podría ser una expresión boolean pero también de tipo int, char, float *...
y la condición se evalúa como true cuando a es distinto de cero (valor entero 0, carácter 0 ó
puntero nulo, en los ejemplos). En C# se clarifica esta ambigüedad y sólo se admiten
expresiones lógicas. De esta manera, la instrucción anterior será válida sólo cuando a sea una
expresión boolean.
Estructuras condicionales
if, if-else
if (a > 0) if (a > 0) {
Console.WriteLine ("Positivo");
Console.WriteLine ("Positivo");
ContPositivos++;
}
if (a > 0) if (a > 0) {
Console.WriteLine ("Positivo");
Console.WriteLine ("Positivo");
else
ContPositivos++;
Console.WriteLine ("No Positivo"); }
else {
Console.WriteLine ("No Positivo");
ContNoPositivos++;
}
if (a > 0) {
Console.WriteLine ("Positivo");
ContPositivos++;
}
else {
if (a < 0) {
Console.WriteLine ("Negativo");
ContNegativos++;
}
else {
Console.WriteLine ("Cero");
ContCeros++;
}
}
switch
using System;
class HolaMundoSwitch
{
public static void Main(String[] args)
{
if (args.Length > 0)
switch(args[0])
{
case "José":
Console.WriteLine("Hola José. Buenos días");
break;
case "Paco":
Console.WriteLine("Hola Paco. Me alegro de verte");
break;
default: Console.WriteLine("Hola {0}", args[0]);
break;
}
else
Console.WriteLine("Hola Mundo");
}
}
Especificar los parámetros al programa en Visual Studio: Utilizar el explorador de soluciones
para configurar las propiedades del proyecto.
using System;
namespace ConsoleApplication14
{
class Class1
{
static int Test(string label)
{
int res;
switch(label)
{
case null:
goto case "A"; // idem case "B" o case "A"
case "B":
case "C":
res = 1;
break;
case "A":
res = 2;
break;
default:
res = 0;
break;
}
return res;
}
while
int i = 0;
while (i < 5) {
...
i++;
}
do...while
int i = 0;
do {
...
i++;
} while (i < 5);
for
int i;
foreach
Un ciclo foreach itera seleccionando todos los miembros de un vector, matriz u otra colección
sin que se requiera explicitar los índices que permiten acceder a los miembros.
El siguiente ejemplo muestra todos los argumentos recibidos por el programa cuando se invoca
su ejecución (argumentos en la línea de órdenes):
El vector (la colección) que se utiliza para iterar es args. Es un vector de datos string. El
ciclo foreach realiza tantas iteraciones como cadenas contenga el vector args, y en cada
iteración toma una y la procesa a través de la variable de control s.
El ciclo foreach proporciona acceso de sólo lectura a los elementos de la colección sobre la
que se itera. Por ejemplo, el código de la izquierda no compilará, aunque el de la derecha sí lo
hará (v es un vector de int):
El ciclo foreach puede emplearse en vectores y colecciones. Una colección es una clase que
implementa el interfaz IEnumerable. Sobre las colecciones dicutiremos más adelante.
Saltos
goto
Aunque el lenguaje lo permita, nunca escribiremos algo como lo que aparece a continuación:
using System;
namespace DemoGoto
{
class MainClass
{
static void Busca(int val, int[,] vector, out int fil, out int col)
{
int i, j;
for (i = 0; i < vector.GetLength(0); i++)
for (j = 0; j < vector.GetLength(1); j++)
if (vector[i, j] == val) goto OK;
throw new InvalidOperationException("Valor no encontrado");
OK:
fil = i; col = j;
}
continue
Mejor no lo usamos.
return
Procuraremos emplearlo sólo al final de un método para facilitar la legibilidad del código.
Tipos de datos
Tipos básicos
El sistema unificado de tipos. El tipo Object
Cadenas de caracteres
Vectores y matrices
Estructuras
Enumeraciones
Cuando definimos un objeto debemos especificar su tipo. El tipo determina qué valores puede
almacenar ese objeto (clase y rango) y las operaciones que pueden efectuarse con él.
La estructura de tipos de C# es una gran novedad ya que establece una relación jerárquica
entre éstos, de manera que todos los tipos son clases y se construyen por herencia de la clase
base Objet. Esta particularidad hace que la creación y gestión de tipos de datos en C# sea
una de las principales novedades y potencialidades del lenguaje.
Tipos básicos
Los tipos de datos básicos son los tipos de datos más comúnmente utilizados en programación.
System.Int32 a = 2;
Al ser un tipo valor no se utiliza el operador new para crear objetos System.Int32, sino que
directamente se indica el literal que representa el valor a crear, con lo que la sintaxis necesaria
para crear entero de este tipo se reduce considerablemente. Es más, dado lo frecuente que es
el uso de este tipo también se ha predefinido en C# el alias int para el mismo, por lo que la
definición de variable anterior queda así de compacta:
int a = 2;
Los tipos están definidos de manera muy precisa y no son dependientes del compilador o de la
plataforma en la que se usan.
La tabla anterior incorpora la columna Símbolo para indicar cómo debe interpretarse un literal.
Por ejemplo, 28UL debe interpretarse como un entero largo sin signo (ulong).
Todos los tipos enumerados son tipos valor, excepto string (que no debe confundirse con un
vector de caracteres) y Object que son tipos referencia. Recuerde, no obstante, que los datos
string se comportaban como un tipo valor ante la asgnación.
El sistema unificado de tipos. El tipo Object
En C# desaparecen las variables y funciones globales: todo el código y todos los datos de
una aplicación forman parte de objetos que encapsulan datos y código (como ejemplo,
recuerde cómo Main() es un método de una clase). Como en otros lenguajes orientados a
objetos, en C# un tipo puede incluir datos y métodos. De hecho, hasta los tipos básicos
predefinidos incluyen métodos, que, como veremos, heredan de la clase base object, a partir
de la cual se construyen implícita o explícitamente todos los tipos de datos. Por ejemplo:
int i = 10;
string c = i.ToString();
e incluso:
string c = 10.ToString();
Esta manera por la que podemos manipular los datos básicos refleja la íntima relación entre
C# y la biblioteca de clase de .NET. De hecho, C# compila sus tipos básicos asociándolos a sus
correspondientes en .NET; por ejemplo, hace corresponder al tipo string) con la clase
System.String, al tipo int) con la clase System.Int32, etc. Así, se confirma que todo es
un objeto en C# (por si aún había alguna duda).
Aunque no comentaremos todos los métodos disponibles para los tipos básicos, destacaremos
algunos de ellos.
Todos los tipos tienen un método ToString() que devuelve una cadena (string)
que representa su valor textual.
char tiene propiedades acerca de su contenido ( IsLetter, IsNumber, etc.) además
de métodos para realizar conversiones (ToUpper(), ToLower(), etc.)
El tipo básico string dispone, como puede suponerse, de muchos métodos
específicos, aún más que la clase string de la biblioteca estándar de C++.
Los tipos numéricos enteros tipos tienen las propiedades MinValue y MaxValue
(p.e. int.MaxValue).
Los tipos float y double tienen la propiedad Epsilon, que indica el mínimo valor
positivo que puede representarse en un dato de su tipo (p.e. float.Epsilon).
Los tipos float y double tienen definidos los valores NaN (no es un número, no
está definido), PositiveInfinite y NegativeInfinity, valores que son pueden ser
devueltos al realizar ciertos cálculos.
Muchos tipos, incluyendo todos los tipos numéricos, proporcionan el método
Parse() que permite la conversión desde un dato string (p.e. double d =
double.Parse("20.5"))
Conversiones de tipos
Implícitas Explícitas
Ocurren automáticamente Requieren un casting
Siempre tienen éxito Pueden fallar
No se pierde información Se puede perder información
int x = 123456;
long y = x; // implicita
short z = (short) x; // explicita (riesgo)
float f1 = 40.0F;
long l1 = (long) f1; // explicita (riesgo por redondeo)
short s1 = (short) l1; // explicita (riesgo por
desbordamiento)
int i1 = s1; // implicita, no hay riesgo
uint i2 = (uint) i1; // explicita (riesgo de error por
signo)
En C# las conversiones de tipo (tanto implícitas como explícitas) en las que intervengan tipos
definidos por el usuario pueden definirse y particularizarse.
Recordemos que pueden emplearse los operadores checked y unchecked para realizar
conversiones (y operaciones aritméticas) en un contexto verificado: el entorno de ejecución
.NET detecta cualquier situación de desbordamiento y lanza una excepción
OverFlowException si ésta se manifiesta.
El tipo object
Por el sistema unificado de tipos de C#, todo es un objeto. C# tiene predefinido un tipo
referencia llamado object y cualquier tipo (valor o referencia, predefinido o definido por el
usuario) es en última instancia, de tipo object (con otras palabras puede decirse que hereda
todas las características de ese tipo).
El tipo object se basa en System.Object de .NET Framework. Las variables de tipo object
pueden recibir valores de cualquier tipo. Todos los tipos de datos, predefinidos y definidos por
el usuario, heredan de la clase System.Object. La clase Object es la superclase
fundamental de todas las clases de .NET Framework; la raíz de la jerarquía de tipos.
Normalmente, los lenguajes no precisan una clase para declarar la herencia de Object porque
está implícita.
object o;
todas estas instrucciones son válidas:
o = 10; // int
o = "Una cadena para el objeto"; // cadena
o = 3.1415926; // double
o = new int [24]; // vector de int
o = false; // boolean
Dado que todas las clases de .NET Framework se derivan de Object, todos los métodos
definidos en la clase Object están disponibles en todos los objetos del sistema. Las clases
derivadas pueden reemplazar, y de hecho reemplazan, algunos de estos métodos, entre los
que se incluyen los siguientes:
public void Object() Inicializa una nueva instancia de la clase Object. Los
constructores llaman a este constructor en clases derivadas, pero éste también puede
utilizarse para crear una instancia de la clase Object directamente.
public bool Equals(object obj) Determina si el objeto especificado es igual al
objeto actual.
protected void Finalize() Realiza operaciones de limpieza antes de que un
objeto sea reclamado automáticamente por el recolector de elementos no utilizados.
public int GetHashCode() Sirve como función hash para un tipo concreto,
apropiado para su utilización en algoritmos de hash y estructuras de datos como las
tablas hash.
public string ToString() Devuelve un objeto string que representa al objeto
actual. Se emplea para crear una cadena de texto legible para el usuario que describe una
instancia de la clase. La implementación predeterminada devuelve el nombre completo del
tipo del objeto Object.
10
Una cadena para el objeto
3,1415926
System.Int32[]
False
public Type GetType() Obtiene el objeto Type que representa el tipo exacto en
tiempo de ejecución de la instancia actual.
System.Int32
System.String
System.Double
System.Int32[]
System.Boolean
Boxing (y su operación inversa, unboxing) permiten tratar a los tipos valor como objetos
(tipo referencia). Los tipos de valor, incluidos los struct y los predefinidos, como int, se
pueden convertir al tipo object (boxing) y desde el tipo object (unboxing).
La posibilidad de realizar boxing permite construir funciones polimórficas: pueden realizar una
operación sobre un objeto sin conocer su tipo concreto.
void Polim(object o)
{
Console.WriteLine(o.ToString());
}
...
Polim(42);
Polim("abcd");
Polim(12.345678901234M);
Polim(new Point(23,45));
Boxing
Boxing es una conversión implícita de un tipo valor al tipo object. Cuando se realiza boxing
de un valor, se asigna una instancia de objeto y se copia el valor en el nuevo objeto.
int i = 123;
La siguiente instrucción aplica implícitamente la operación de boxing sobre la variable i:
object o = i;
El resultado de esta instrucción es crear un objeto o en la pila que hace referencia a un valor
del tipo int alojado en el heap. Este valor es una copia del valor del tipo de valor asignado a
la variable i. La diferencia entre las dos variables, i y o se muestra en la siguiente figura:
int i = 123;
object o = (object) i;
Unboxing
Una vez que se ha hecho boxing sobre un dato y disponemos de un object no puede hacerse
demasiado con él ya que no pueden emplearse métodos o propiedades del tipo original: el
compilador no puede conocer el tipo original sobre el que se hizo boxing.
string s1 = "Hola";
object o = s1; // boxing
...
if (o.Lenght > 0) // ERROR
Una vez que se ha hecho boxing puede deshacerse la conversión (unboxing) haciendo casting
explícito al tipo de dato inicial.
string s2;
if (o is string)
s2 = (string) o; // unboxing
o alternativamente:
string s2 = o as string; // conversion
if (s2 != null) // OK, la conversion funciono
Conclusiones
Ventajas del sistema unificado de tipos: las colecciones funcionan sobre cualquier tipo.
t.Add(0, "zero");
t.Add(1, "one");
t.Add(2, "two");
La necesidad de utilizar boxing disminuirá cuando el CLR permita genéricos (algo similar a los
templates en C++).
Cadenas de caracteres
Una cadena de caracteres no es más que una secuencia de caracteres Unicode. En C# se
representan mediante objetos del tipo string, que no es más que un alias del tipo
System.String incluido en la BCL.
El tipo string es un tipo referencia. Representa una serie de caracteres inmutable. Se dice
que una instancia de String es inmutable porque no se puede modificar su valor una vez
creada. Los métodos que aparentemente modifican una cadena devuelven en realidad una
cadena nueva que contiene la modificación.
Puede accederse a cada uno de los caracteres de la cadena mediante índice, como ocurre
habitualmente en otros lenguajes, siendo la primera posición asociada al índice cero. Este
acceso, no obstante, sólo está permitido para lectura.
Por definición, un objeto String, incluida la cadena vacía (""), es mayor que una referencia
nula y dos referencias nulas son iguales entre sí. El carácter null se define como el
hexadecimal 0x00. Puede consultarse si una cadena es vacía empleando la propiedad estática
(sólo lectura) Empty: el valor de este campo es la cadena de longitud cero o cadena vacía, "".
Una cadena vacía no es igual que una cadena cuyo valor sea null.
string linea;
string [] palabras;
...
palabras = linea.Split (null); // null indica dividir por espacios
En definitiva, la clase String incluye numerosos métodos, que pueden emplease para:
Todas las tablas que definamos, sea cual sea el tipo de elementos que contengan, son objetos
que derivan de System.Array. Ese espacio de nombres proporciona métodos para la
creación, manipulación, búsqueda y ordenación de matrices, por lo tanto, sirve como clase
base para todas las matrices de la CLR (Common Language Runtime).
En C# las tablas pueden ser multidimensionales, se accede a los elementos por índice, siendo
el índice inicial de cada dimensión 0.
Siempre se comprueba que se esté accediendo dentro de los límites. Si se intenta acceder a un
elemento de un vector (matriz) especificando un índice fuera del rango, se detecta en tiempo
de ejecución y se lanza una excepción IndexOutOfBoundsException.
La sintaxis es ligeramente distinta a la del C++ porque las tablas son objetos de tipo
referencia:
Los vectores pueden dimensionarse dinámicamente (en tiempo de ejecución). Por ejemplo, el
siguiente código:
Console.ReadLine();
Esta facilidad es una gran ventaja, aunque una vez que el constructor actúa y se crea una
instancia de la clase System.Array
Matrices
Las diferencias son importantes respecto a C++ ya que C# permite tanto matrices
rectangulares (todas las filas tienen el mismo número de columnas) como matrices dentadas o
a jirones.
Una matriz dentada no es más que una tabla cuyos elementos son a su vez tablas,
pudiéndose así anidar cualquier número de tablas. Cada tabla puede tener un número propio
de casillas. En el siguiente ejemplo se pide el número de filas, y para cada fila, el número de
casillas de ésa.
// Mostrar resultado
Console.WriteLine ("Contenido de la matriz: ");
for (int f=0; f<TablaDentada.Length; f++)
{
for (int c=0; c<TablaDentada[f].Length; c++)
Console.Write (TablaDentada[f][c] + " ");
Console.WriteLine();
}
Console.ReadLine();
El resultado es:
Introduzca las dimensiones:
Num. Filas: 4
Num. Cols. de fila 0: 2
Num. Cols. de fila 1: 5
Num. Cols. de fila 2: 3
Num. Cols. de fila 3: 7
Contenido de la matriz:
11 12
21 22 23 24 25
31 32 33
41 42 43 44 45 46 47
Estructuras
Una estructura (struct) se emplea para definir nuevos tipos de datos. Los nuevos tipos así
definidos son tipos valor (se almacenan en la pila).
La sintaxis para la definición de estructuras es similar a la empleada para las clases (se emplea
la palabra reservada struct en lugar de class). No obstante,
Sobre esta declaración de tipo Point, observe estas declaraciones de datos Point:
Point p3;
crea una instancia de la clase Point en la pila sin iniciar (cuidado: no inicia p3 a null, no es
un tipo referencia). Por lo tanto, la instrucción:
Console.WriteLine ("p3 = " + p3.Valor());
produce un error de compilación, al intentar usar una variable no asignada.
p.x += 100;
int px = p.x; // p.x==102
p3.x = px; // p3.x==102
p2 = p; // p2.x==102, p2.y==5
p2.x += 100; // p2.x==202, p2.y==5
p3.y = p.y + p2.y; // p3.y==10
p = [102,5]
p2 = [202,5]
p3 = [102,10]
Enumeraciones
Una enumeración o tipo enumerado es un tipo especial de estructura en la que los literales
de los valores que pueden tomar sus objetos se indican explícitamente al definirla.
enum Tamanio {
Pequeño = 1,
Mediano = 3,
Grande = 5
Inmenso
}
En el ejemplo, el valor asociado a Inmenso es 6.
La clase Enum (System.Enum) proporciona la clase base para las enumeraciones. Proporciona
métodos que permiten comparar instancias de esta clase, convertir el valor de una instancia en
su representación de cadena, convertir la representación de cadena de un número en una
instancia de esta clase y crear una instancia de una enumeración y valor especificados. Por
ejemplo:
Color c1 = Color.Black;
Console.WriteLine((int) c1); // 0
Console.WriteLine(c1); // Black
Console.WriteLine(c1.ToString()); // Black
Color c2 = Color.White;
Console.WriteLine((int) c2); // 7
Console.WriteLine(c2.ToString()); // White
enum ModArchivo
{
Lectura = 1,
Escritura = 2,
Oculto = 4,
Sistema = 8
}
...
ModArchivo st = ModArchivo.Lectura | ModArchivo.Escritura;
...
Console.WriteLine (st.ToString("F")); // Lectura,
Escritura
Console.WriteLine (Enum.Format(typeof(ModArchivo), st, "G")); // 3
Console.WriteLine (Enum.Format(typeof(ModArchivo), st, "X")); // 00000003
Console.WriteLine (Enum.Format(typeof(ModArchivo), st, "D")); // 3
Clases (1)
Introducción
Un objeto es un agregado de datos y de métodos que permiten manipular dichos datos, y un
programa en C# no es más que un conjunto de objetos que interaccionan unos con otros a
través de sus métodos.
Datos
o campos
o constantes y campos de sólo lectura
o propiedades
Métodos
o métodos generales
o métodos constructores
o métodos de sobrecarga de operadores e indexadores
Un campo es un dato común a todos los objetos de una clase. La declaración de un dato se
realiza, sintácticamente, como cualquier variable.
La palabra reservada this es una variable predefinida disponible dentro de las funciones no
estáticas de un tipo que se emplea, en un método de una clase, para acceder a los miembros
del objeto sobre el que se está ejecutando el método en cuestión. En definitiva, permite
acceder al objeto "activo". El siguiente ejemplo muestra de manera clara su aplicación:
// Método
public void MuestraCoche ()
{
Console.WriteLine (this.Marca + " " + this.Modelo +
" (" + this.VelocMax + " Km/h)");
}
} // class CocheSimple
recordemos que el operador new se emplea para crear objetos de una clase especificada.
Cuando se ejecuta se llama a un método especial llamado constructor y devuelve una
referencia al objeto creado. Si no se especifica ningún constructor C# considera que existe un
constructor por defecto sin parámetros. Una buena costumbre es proporcionar siempre algún
constructor.
Una aplicación que usa la clase CocheSimple
class CocheSimpleApp
{
static void Main(string[] args)
{
// "MiCoche" y "TuCoche" son variables de tipo "CocheSimple"
// que se inicializan llamando al constructor.
CocheSimple MiCoche = new CocheSimple ("Citröen", "Xsara", 220);
CocheSimple TuCoche = new CocheSimple ("Opel", "Corsa", 190);
Console.ReadLine ();
} // Main
} // class CocheSimpleApp
Modificadores de acceso
Los modificadores de acceso nos permiten especificar quién puede usar un tipo o un miembro
del tipo, de forma que nos permiten gestionar la encapsulación de los objetos en nuestras
aplicaciones:
Se pueden definir miembros estáticos que son comunes a todas las instancias de la clase.
Lógicamente, los métodos estáticos no pueden acceder a variables de instancia, ni a la variable
this que hace referencia al objeto actual.
using System;
class Mensaje {
public static string Bienvenida = "¡Hola!, ¿Cómo está?";
public static string Despedida = "¡Adios!, ¡Vuelva pronto!";
}
class MiembrosStaticApp
{
static void Main()
{
Console.WriteLine(Mensaje.Bienvenida);
Console.WriteLine(" Bla, bla, bla ... ");
Console.WriteLine(Mensaje.Despedida);
Console.ReadLine();
}
}
De cualquier forma, no conviene abusar de los miembros estáticos, ya que son básicamente
datos y funciones globales en entornos orientados a objetos.
Campos, constantes, campos de sólo lectura y
propiedades
Campos
Un campo es una variable que almacena datos, bien en una una clase, bien en una estructura.
Constantes (const)
Una constante es un dato cuyo valor se evalúa en tiempo de compilación y, por tanto, es
implícitamente estático (p.ej. Math.PI).
public MiClase(string s)
{
s1 = s;
}
}
......
MiClase mio = new MiClase ("Prueba");
Console.WriteLine(mio.s1);
Console.WriteLine(MiClase.d1);
......
Prueba
1,22460635382238E-16
Propiedades
// Propiedad
public float MaxSpeed
{
get { return VelocMax / 1.6F; }
set { VelocMax = (int) ((float) value * 1.6F);}
}
de manera que si las siguientes líneas se añaden al final del método main en
CocheSimpleApp:
Console.WriteLine ();
Console.WriteLine ("My car's Max Speed: "
+ MiCoche.MaxSpeed +" Mph" ); // get
Console.WriteLine ();
Console.Write ("Mi coche: ");
MiCoche.MuestraCoche(); // LLamada al método "MuestraCoche()"
el resultado obtenido es:
Mi coche: Citröen Xsara (220 Km/h)
El tuyo: Opel Corsa (190 Km/h)
using System;
class Coche
{
// Campos
protected double velocidad=0;
public string Marca;
public string Modelo;
public string Color;
public string NumBastidor;
// Método constructor
public Coche(string marca, string modelo,
string color, string numbastidor)
{
this.Marca=marca;
this.Modelo=modelo;
this.Color=color;
this.NumBastidor=numbastidor;
}
// Método
public void Acelerar(double c)
{
Console.WriteLine("--> Incrementando veloc. en {0} km/h", c);
this.velocidad += c;
}
// Método
public void Girar(double c)
{
Console.WriteLine("--> Girando {0} grados", c);
}
// Método
public void Frenar(double c)
{
Console.WriteLine("--> Reduciendo veloc. en {0} km/h", c);
this.velocidad -= c;
}
// Método
public void Aparcar()
{
Console.WriteLine("-->Aparcando coche");
this.velocidad = 0;
}
} // class Coche
class EjemploCocheApp
{
MiCoche.Acelerar(100);
Console.WriteLine("La velocidad actual es de {0} km/h",
MiCoche.Velocidad);
MiCoche.Frenar(75);
Console.WriteLine("La velocidad actual es de {0} km/h",
MiCoche.Velocidad);
MiCoche.Girar(45);
MiCoche.Aparcar();
Console.WriteLine("La velocidad actual es de {0} km/h",
MiCoche.Velocidad);
Console.ReadLine();
}
} // class EjemploCocheApp
Métodos
Implementan las operaciones que se pueden realizar con los objetos de un tipo concreto.
Constructores, destructores y operadores son casos particulares de métodos. Las propiedades
y los indexadores se implementan con métodos (get y set).
Como en cualquier lenguaje de programación, los métodos pueden tener parámetros, contener
órdenes y devolver un valor (con return).
Por defecto, los parámetros se pasan por valor (por lo que los tipos "valor" no podrían
modificarse en la llamada a un método). El modificador ref permite que pasemos parámetros
por referencia. Para evitar problemas de mantenimiento, el modificador ref hay que
especificarlo tanto en la definición del método como en el código que realiza la llamada.
Además, la variable que se pase por referencia ha de estar inicializada previamente.
int x = 10;
El siguiente código emplea una función que suma todos los parámetros que recibe. El número
de éstos es indeterminado, aunque debe asegurarse que sean de tipo int:
C# permite especificar código para inicializar una clase mediante un constructor estático. El
constructor estático se invoca una única vez, antes de llamar al constructor de una instancia
particular de la clase o a cualquier método estático de la clase. Sólo puede haber un
constructor estático por tipo y éste no puede tener parámetros.
Destructores: Se utilizan para liberar los recursos reservados por una instancia (justo antes
de que el recolector de basura libere la memoria que ocupe la instancia).
A diferencia de C++, la llamada al destructor no está garantizada por lo que tendremos que
utilizar una orden using e implementar el interfaz IDisposable para asegurarnos de que se
liberan los recursos asociados a un objeto). Sólo las clases pueden tener destructores (no los
struct).
class Foo
{
~Foo()
{
Console.WriteLine("Destruido {0}", this);
}
}
Sobrecarga de operadores
Como en C++, se pueden sobrecargar (siempre con un método static) algunos operadores
unarios (+, -, !, ~, ++, --, true, false) y binarios (+, -, *, /, %, &, |, ^, ==, !=, <, >, <=,
>=, <, >).
using System;
class OverLoadApp
{
// Operadores de igualdad
// Operadores aritméticos
public static Point operator + (Point p1, Point p2)
{
return new Point(p1.x+p2.x, p1.y+p2.y);
}
public static Point operator - (Point p1, Point p2)
{
return new Point(p1.x-p2.x, p1.y-p2.y);
}
}
Console.WriteLine ();
Point p4 = p1 + p3;
Console.WriteLine ("p4 (p1+p3) es: ({0},{1})", p4.X,
p4.Y);
Point p5 = p1 - p1;
Console.WriteLine ("p5 (p1-p1) es: ({0},{1})", p5.X,
p5.Y);
Console.WriteLine ();
Console.WriteLine ("p1 es: ({0},{1})", p1.X, p1.Y);
Console.WriteLine ("p2 es: ({0},{1})", p2.X, p2.Y);
Console.WriteLine ("p3 es: ({0},{1})", p3.X, p3.Y);
Console.WriteLine ("p4 es: ({0},{1})", p4.X, p4.Y);
Console.WriteLine ("p5 es: ({0},{1})", p5.X, p5.Y);
Console.ReadLine ();
}
}
Para asegurar la compatibilidad con otros lenguajes de .NET:
// Operadores aritméticos
// Operadores de igualdad
using System;
class ConversionesApp
{
} // class Euro
Console.ReadLine ();
}
}
C# no permite definir conversiones entre clases que se relacionan mediante herencia. Dichas
conversiones están ya disponibles: de manera implícita desde una clase derivada a una
antecesora y de manera explícita a la inversa.
Clases y structs
Tanto las clases como los structs permiten al usuario definir sus propios tipos, pueden
implementar múltiples interfaces y pueden contener datos (campos, constantes, eventos...),
funciones (métodos, propiedades, indexadores, operadores, constructores, destructores y
eventos) y otros tipos internos (clases, structs, enums, interfaces y delegados).
using System;
struct SPoint
{
private int x, y; // Campos
class CPoint
{
private int x, y; // Campos
class Class2App
{
static void Main(string[] args)
{
SPoint sp = new SPoint(2,5);
sp.X += 100;
int spx = sp.X; // spx = 102
Aunque las coincidencias son muchas, existen, no obstante, existen algunas diferencias entre
ellos:
Cuando se crea un objeto struct mediante el operador new, se crea y se llama al constructor
apropiado. A diferencia de las clases, se pueden crear instancias de las estructuras sin utilizar
el operador new. Si no se utiliza new, los campos permanecerán sin asignar y el objeto no se
podrá utilizar hasta haber inicializado todos los campos.
Clase Struct
Tipo referencia Tipo valor
Para las estructuras no existe herencia: una
estructura no puede heredar de otra estructura o
Puede heredar de
clase, ni puede ser la base de una clase. Sin
otro tipo (que no
embargo, las estructuras heredan de la clase base
esté "sellado")
Object. Una estructura puede implementar
interfaces del mismo modo que las clases.
Puede tener un
constructor sin No puede tener un constructor sin parámetros
parámetros
No pueden Se pueden crear instancias de las estructuras sin
crearse instancias utilizar el operador new, pero los campos
sin emplear el permanecerán sin asignar y el objeto no se podrá
operador new. utilizar hasta haber iniciado todos los campos.
Puede tener un
No puede tener destructor
destructor
Herencia
Concepto de herencia
Clases abstractas
Clases selladas
Tipos anidados
Concepto de herencia
El mecanismo de herencia es uno de los pilares fundamentales en los que se basa la
programación orientada a objetos. Es un mecanismo que permite definir nuevas clases a partir
de otras ya definidas. Si en la definición de una clase indicamos que ésta deriva de otra,
entonces la primera -a la que se le suele llamar clase hija o clase derivada- será tratada por
el compilador automáticamente como si su definición incluyese la definición de la segunda -a la
que se le suele llamar clase padre o clase base.
A los miembros definidos en la clase hija se le añadirán los que hubiésemos definido en la
clase padre: la clase derivada "hereda" de la clase base.
La palabra clave base se utiliza para obtener acceso a los miembros de la clase base desde
una clase derivada.
Herencia de constructores
Los objetos de una clase derivada contarán con los mismos miembros que los objetos de la
clase base y además incorporarán nuevos campos y/o métodos. El constructor de una clase
derivada puede emplear el constructor de la clase base para inicializar los campos heredados
de la clase padre con la construcción base. En realidad se trata de una llamada al constructor
de la clase base con los parámetros adecuados.
: base(<parametrosBase>)
public class B
{
private int h; // Campo
} // class D
......
B varB1 = new B(); // Const. sin parámetros de B
B varB2 = new B(5); // Const. con 1 parámetro de B
Console.WriteLine("varB1 : (H={0})", varB1.H);
Console.WriteLine("varB2 : (H={0})\n", varB2.H);
using System;
namespace DemoHerencia {
class CocheSimple
{
private int VelocMax;
private string Marca;
private string Modelo;
public CocheSimple () {
this.VelocMax = 0;
this.Marca = "??";
this.Modelo = "??";
}
public CocheSimple (string marca, string mod, int velMax)
{
this.VelocMax = velMax;
this.Marca = marca;
this.Modelo = mod;
}
} // class CocheSimple
public Taxi () {}
public Taxi (string marca, string mod, int vel,
string lic) : base (marca, mod, vel)
{
this.CodLicencia = lic;
}
public string Licencia {
get { return this.CodLicencia; }
}
} // class Taxi
class DemoHerenciaApp {
CocheSimple MiCoche =
new CocheSimple ("Citröen", "Xsara Picasso", 220);
CocheSimple TuCoche =
new CocheSimple ("Opel", "Corsa", 190);
CocheSimple UnCoche = new CocheSimple ();
Console.WriteLine();
Console.ReadLine ();
} // Main
} // class DemoHerenciaApp
} // namespace DemoHerencia
Redefinición de métodos
Siempre que se redefine un método que aparece en la clase base, hay que utilizar
explícitamente la palabra reservada override y, de esta forma, se evitan redefiniciones
accidentales (una fuente de errores en lenguajes como Java o C++).
Sabemos que todos los objetos (incluidas las variables de los tipos predefinidos) derivan, en
última instancia, de la clase Object. Esta clase proporciona el método ToString que crea
una cadena de texto legible para el usuario que describe una instancia de la clase. Si dejamos
sin redefinir este método y empleando la clase CocheSimple las siguientes instrucciones:
CocheSimple MiCoche =
new CocheSimple ("Citröen", "Xsara Picasso", 220);
Mi coche: DemoHerencia.CocheSimple
class CocheSimple
{
...
public override string ToString()
{
return (this.Marca + " " + this.Modelo +
" (" + this.VelocMax + " Km/h)");
}
...
}
por lo que podemos sutituir las instrucciones que muestran los datos de los objetos
CocheSimple por:
class CocheSimple
{
...
public override string ToString()
{
return (this.Marca + " " + this.Modelo +
" (" + this.VelocMax + " Km/h)");
}
...
}
class Taxi : CocheSimple
{
...
public override string ToString()
{
return (base.ToString() + "\n Licencia: " +
this.Licencia);
}
...
}
......
Taxi ElTaxiDesconocido = new Taxi ();
Console.WriteLine ("Un taxi sin identificar: " +
ElTaxiDesconocido);
y el resultado es:
Ahora las instrucciones de escritura se convierten en llamadas a este método, por ejemplo:
Métodos virtuales
Un método es virtual si puede redefinirse en una clase derivada. Los métodos son no
virtuales por defecto.
void HandleShape(Shape s)
{
...
s.Draw(); // Polimorfismo
...
}
HandleShape(new Box());
HandleShape(new Sphere());
HandleShape(new Shape());
Permiten incluir métodos abstractos y métodos no abstractos cuya implementación hace que
sirvan de clases base (herencia de implementación). Como es lógico, no pueden estar
"selladas".
Métodos abstractos
Un método abstracto es un método sin implementación que debe pertenecer a una clase
abstracta. Lógicamente se trata de un método virtual forzoso y su implementación se realizará
en una clase derivada.
void HandleShape(Shape s)
{
...
s.Draw();
...
}
HandleShape(new Box());
HandleShape(new Sphere());
HandleShape(new Shape()); // Error !!!
Clases selladas
Una clase sellada (sealed), es una clase de la que no pueden derivarse otras clases (esto es,
no puede utilizarse como clase base). Obviamente, no puede ser una clase abstracta.
¿Para qué sirve sellar clases? Para evitar que se puedan crear subclases y optimizar el código
(ya que las llamadas a las funciones de una clase sellada pueden resolverse en tiempo de
compilación).
Tipos anidados
C# permite declarar tipos anidados, esto es, tipos definidos en el ámbito de otro tipo. El
anidamiento nos permite que el tipo anidado pueda acceder a todos los miembros del tipo que
lo engloba (independientemente de los modificadores de acceso) y que el tipo esté oculto de
cara al exterior (salvo que queramos que sea visible, en cuyo caso habrá que especificar el
nombre del tipo que lo engloba para poder acceder a él).
Clases (2)
Indexadores (indexers)
C# no permite, hablado con rigor, la sobrecarga del operador de acceso a tablas [ ]. Si
permite, no obstante, definir lo que llama un indexador para una clase que permite la misma
funcionalidad.
Los indexadores permiten definir código a ejecutar cada vez que se acceda a un objeto del tipo
del que son miembros usando la sintaxis propia de las tablas, ya sea para leer o escribir. Esto
es especialmente útil para hacer más clara la sintaxis de acceso a elementos de objetos que
puedan contener colecciones de elementos, pues permite tratarlos como si fuesen tablas
normales.
A diferencia de las tablas, los índices que se les pase entre corchetes no tiene porqué ser
enteros, pudiéndose definir varios indexadores en un mismo tipo siempre y cuando cada uno
tome un número o tipo de índices diferente.
El código que aparece en el bloque get se ejecuta cuando la expresión MiObjeto[x] aparece
en la parte derecha de una expresión mientras que el código que aparece en el bloque set se
ejecuta cuando MiObjeto[x] aparece en la parte izquierda de una expresión.
Igual que las propiedades, pueden ser de sólo lectura, de sólo escritura o de lectura y
escritura.
El nombre dado a un indexador siempre ha de ser this.
Lo que diferenciará a unos indexadores de otros será el número y tipo de sus índices.
Ejemplo de indexador
using System;
namespace IndexadorCoches
{
public class Coche
{
private int VelocMax;
private string Marca;
private string Modelo;
} // class Coche
} // class Coches
class IndexadorCochesApp
{
static void Main(string[] args)
{
// Crear una colección de coches
Coches MisCoches = new Coches (); // Por defecto (10)
MisCoches.NumCoches = 3;
// Mostrar la colección
Console.WriteLine ();
for (int i=0; i<MisCoches.NumCoches; i++)
{
Console.Write ("Coche Num.{0}: ", i+1);
Console.WriteLine (MisCoches[i]); // Acceso ("get")
}
Console.ReadLine ();
// *********************************************
// Mostrar la colección
Console.WriteLine ();
for (int i=0; i<TusCoches.NumCoches; i++)
{
Console.Write ("Coche Num.{0}: ", i+1);
Console.WriteLine (TusCoches[i]); // Acceso ("get")
}
Console.ReadLine ();
} // Main
} // class IndexadorCochesApp
} // namespace IndexadorCoches
Interfaces
Un interfaz define un contrato semántico que ha de respetar cualquier clase (o struct) que
implemente el interfaz.
Una interfaz puede verse como una forma especial de definir clases que sólo cuenten con
miembros abstractos. Sin embargo, todo tipo que derive de una interfaz ha de dar una
implementación de todos los miembros que hereda de esta, y no como ocurre con las clases
abstractas donde es posible no darla si se define como abstracta también la clase hija.
Ejemplo de polimorfismo
using System;
namespace Interface1
{
class Interface1App
{
// Definición de una interface
// Ejemplo de polimorfismo
demo = c1;
demo.MetodoDeIDemo();
demo = c2;
demo.MetodoDeIDemo();
Console.ReadLine();
}
}
}
Otro ejemplo:
using System;
class InterfaceApp
{
interface IPresentable
{
void Presentar();
}
Console.ReadLine();
}
}
El principal uso de las interfaces es indicar que una clase implementa ciertas características.
Por ejemplo, el ciclo foreach trabaja internamente comprobando que la clase sobre la que se
aplica implementa el interfaz IEnumerable y llamando a los métodos definidos en esa
interfaz.
......
// Operadores relacionales
......
} // class Point
Herencia múltiple
Herencia de interfaces
using System;
namespace Interface2
{
class Interface2App
{
// Definición de interfaces
}
static void Main(string[] args)
{
Clase1 c1 = new Clase1();
Clase2 c2 = new Clase2();
Clase3 c3 = new Clase3();
IDemo1 i1;
IDemo2 i2;
IDemo3 i3;
c1.Metodo1DeInterface1();
Console.WriteLine(c1.Metodo2DeInterface1());
Console.WriteLine();
i1 = c3;
Console.WriteLine("Cuando i1 = c3 ");
i1.Metodo1DeInterface1();
Console.WriteLine(i1.Metodo2DeInterface1());
Console.WriteLine();
i3 = c3;
Console.WriteLine("Cuando i3 = c3 ");
i3.Metodo1DeInterface1();
Console.WriteLine(i3.Metodo2DeInterface1());
i3.Metodo1DeInterface3("Aplicado a i3: ");
Console.WriteLine();
i1 = c2;
Console.WriteLine("Cuando i1 = c2 ");
i1.Metodo1DeInterface1();
Console.WriteLine(i1.Metodo2DeInterface1());
i2 = c2;
Console.WriteLine("Ahora i2 = c2 ");
i2.Metodo1DeInterface2();
Console.WriteLine();
Console.ReadLine();
}
}
}
interface IControl
{
void Delete();
}
Un delegado crea un nuevo tipo al que debe amoldarse un método para que pueda ser
asignado al delegado. Su función es similar a la de los punteros a funciones en lenguajes como
C y C++ (C# no soporta punteros a funciones) sólo que los delegados son a prueba de tipos,
orientados a objetos y seguros.
Los delegados pueden pasarse a métodos y pueden usarse para llamar a los métodos de los
que contienen referencias. Los delegados proporcionan polimorfismo para las llamadas a
funciones:
Un "tipo" delegado
using System;
class Delegate1App
{
// Declaración del "tipo delegado" llamado "Del":
// funciones que devuelven un double y reciben un
double.
// Llamadas
Console.WriteLine (del1(0)); // 0
Console.WriteLine (del2(0)); // 1
Console.WriteLine (del3(10)); // 11
Console.ReadLine();
}
}
En el ejemplo anterior Sin, Cos e incremento son métodos con la misma signatura (todos
reciben un único argumento de tipo double) y devuelven un valor de tipo double. Los
objetos del1, del2 y del3 son objetos delegados del tipo Del (Sin, Cos e incremento
delegan en éstos) que invocan a los métodos a los que representan.
Para hacer más evidente el polimorfismo el método Main podría escribirse como:
Del f;
f = f1; Console.WriteLine (f(0)); // 0
f = f2; Console.WriteLine (f(0)); // 1
f = f3; Console.WriteLine (f(10)); // 11
}
En este caso f referencia, por turno, a los métodos representados por f1, f2 y f3.
Los delegados son muy útiles ya que permiten disponer de objetos cuyos métodos puedan ser
modificados dinámicamente durante la ejecución de un programa.
En general, son útiles en todos aquellos casos en que interese pasar métodos como
parámetros de otros métodos. Observe el siguiente ejemplo:
using System;
using System.IO;
class DelegateTest
{
Console.ReadLine();
}
}
Las instrucciones:
"Multicasting"
using System;
class Delegate2App
{
delegate void SomeEvent (void);
Console.WriteLine("Llamada a func");
func(); // Se llama tanto a Func1 como a Func2
Console.WriteLine("Llamada a func");
func(); // Sólo se llama a Func2
Console.ReadLine();
}
}
El siguiente ejemplo ilustra de una manera más compleja la adición y eliminación de delegados
a un delegado multimiembro. Observe como interviene un objeto de la clase
EjemploDelegado que encapsula dos delegados.
"Multicasting" (2)
using System;
class Multicasting
{
public delegate void Calculo (int n1, int n2, ref int res);
public void Suma (int n1, int n2, ref int res)
{
res = n1 + n2;
Console.WriteLine("Suma ({0},{1}) = {2}", n1, n2, res);
}
public void Resta (int n1, int n2, ref int res)
{
res = n1 - n2;
Console.WriteLine("Suma ({0},{1}) = {2}", n1, n2, res);
}
MultiCalc -= Del.Calculo1;
MultiCalc (5, 9, ref resultado);
Console.ReadLine();
}
}
Siempre se pueden utilizar interfaces en vez de delegados. Los interfaces son más versátiles,
ya que pueden encapsular varios métodos y permiten herencia, si bien los delegados resultan
más adecuados para implementar manejadores de eventos. Con los delegados se escribe
menos código y se pueden implementar fácilmente múltiples manejadores de eventos en una
única clase.
Eventos
Los delegados son la base sobre la que se monta la gestión de eventos en la plataforma .NET.
Muchas aplicaciones actuales se programan en función de eventos. Cuando se produce algún
hecho de interés para nuestra aplicación, éste se notifica mediante la generación de un evento,
el cual será procesado por el manejador de eventos correspondiente (modelo "publish-
subscribe"). Los eventos nos permiten enlazar código personalizado a componentes creados
previamente (mecanismo de "callback").
El callback consiste en que un cliente notifica a un servidor que desea ser informado cuando
alguna acción tenga lugar. C# usa los eventos de la misma manera que Visual Basic usa los
mensajes.
Las aplicaciones en Windows se programan utilizando eventos, pues los eventos resultan
especialmente indicados para la implementación de interfaces interactivos. Cuando el usuario
hace algo (pulsar una tecla, hacer click con el ratón, seleccionar un dato de una lista...), el
programa reacciona en función de la acción del usuario.
Eventos en C#
public MyForm()
{
okButton = new Button(...);
okButton.Caption = "OK";
okButton.Click += new EventHandler(OkClicked);
}
}
Aspectos avanzados de C#
Excepciones
Las excepciones ofrecen varias ventajas respecto a otros métodos de notificación de error,
como los códigos devueltos (órdenes return) ya que ningún error pasa desapercibido (las
excepciones no pueden ser ignoradas) y no tienen por qué tratarse en el punto en que se
producen. Los valores no válidos no se siguen propagando por el sistema. No es necesario
comprobar los códigos devueltos. Es muy sencillo agregar código de control de excepciones
para aumentar la confiabilidad del programa.
El proceso es el siguiente:
La sentencia throw lanza una excepción (una instancia de una clase derivada
de System.Exception, que contiene información sobre la excepción:
Message, StackTrace, HelpLink, InnerException...).
El bloque try delimita código que podría generar una excepción.
El bloque catch indica cómo se manejan las excepciones. Se puede relanzar
la excepción capturada o crear una nueva si fuese necesario. Se pueden
especificar distintos bloques catch para capturar distintos tipos de
excepciones. En ese caso, es recomendable poner primero los más
específicos (para asegurarnos de que capturamos la excepción concreta).
El bloque finally incluye código que siempre se ejecutará (se produzca o no
una excepción).
Ejemplo 1
El siguiente ejemplo pone de manifiesto el flujo de ejecución que sigue un programa que lanza
y procesa una excepción.
try {
Console.WriteLine("try");
throw new Exception("Mi excepcion");
}
catch {
Console.WriteLine("catch");
}
finally {
Console.WriteLine("finally");
}
La ejecución de este programa produce el siguiente resultado:
try
catch
finally
Ejemplo 2
La clase Exception es la clase base de la que derivan las excepciones. La mayoría de los
objetos de excepción son instancias de alguna clase derivada de Exception, pero se puede
iniciar cualquier objeto derivado de la clase Object como excepción. En casi todos los casos,
es recomendable iniciar y detectar sólo objetos Exception.
La clase Exception tiene varias propiedades que facilitan la comprensión de una excepción.
Entre éstas destacamos la propiedad Message. Esta propiedad proporciona información sobre
la causa de una excepción. Veamos cómo se utiliza:
try {
Console.WriteLine("try");
throw new Exception("Mi excepcion");
}
catch (Exception e)
{
Console.WriteLine("catch");
Console.WriteLine("Excepción detectada: " + e.Message);
}
catch {
Console.WriteLine("catch");
}
finally {
Console.WriteLine("finally");
}
try
catch
Excepción detectada: Mi excepcion
finally
Ejemplo 3
try
{
Console.WriteLine("--> try");
denominador = Convert.ToInt16(strDen);
cociente = numerador / denominador;
Console.WriteLine ("Cociente = {0}", cociente);
}
catch (ArithmeticException e)
{
Console.WriteLine("--> catch");
Console.WriteLine("Excep. aritmética");
Console.WriteLine("ArithmeticException Handler: {0}",
e.ToString());
}
catch (ArgumentNullException e)
{
Console.WriteLine("--> catch");
Console.WriteLine("Excep. de argumento nulo");
Console.WriteLine("ArgumentNullException Handler: {0}",
e.ToString());
}
catch (Exception e)
{
Console.WriteLine("--> catch");
Console.WriteLine("generic Handler: {0}",
e.ToString());
}
finally
{
Console.WriteLine("--> finally");
}
Console.ReadLine();
}
}
Ejemplo 4
Hemos visto que pueden conocerse los detalles de la excepción que se haya producido.
Podemos conocer más detalles usando la propiedad StackTrace. Esta propiedad contiene un
seguimiento de pila que se puede utilizar para determinar dónde se ha producido un error. El
seguimiento de pila contiene el nombre del archivo de código fuente y el número de la línea
del programa si está disponible la información de depuración.
using System;
using System.Diagnostics;
class ExcepApp
{
static void Main(string[] args)
{
int numerador = 10;
Console.WriteLine ("Numerador es = {0}", numerador);
try
{
Console.WriteLine("--> try");
denominador = Convert.ToInt16(strDen);
cociente = numerador / denominador;
Console.WriteLine ("Cociente = {0}", cociente);
}
catch (Exception e)
{
Console.WriteLine("--> catch");
Console.WriteLine("Generic Handler: {0}", e.ToString());
Console.WriteLine();
Console.WriteLine("Traza de la pila:");
for (int i = 0; i < st.FrameCount; i++) {
StackFrame sf = st.GetFrame(i);
Console.WriteLine(" Pila de llamadas, Método: {0}",
sf.GetMethod() );
Console.WriteLine(" Pila de llamadas, Línea : {0}",
sf.GetFileLineNumber());
}
Console.WriteLine();
}
finally
{
Console.WriteLine("--> finally");
}
Console.ReadLine();
}
}
Reflexión
La capacidad de reflexión de la plataforma .NET (similar a la de la plataforma Java) nos
permite explorar información sobre los tipos de los objetos en tiempo de ejecución.
La instrucción GetType() obtiene el objeto Type de la instancia actual sobre el que se aplica.
El valor devuelto es representa el tipo exacto, en tiempo de ejecución, de la instancia actual.
Un sencillo ejemplo con Type y GetType()
Using System;
public class ClaseBase : Object {}
public class ClaseDerivada : ClaseBase {}
public class Test {
public static void Main() {
ClaseBase ibase = new ClaseBase();
ClaseDerivada iderivada = new ClaseDerivada();
Console.WriteLine("ibase: Type is {0}", ibase.GetType());
Console.WriteLine("iderivada: Type is {0}", iderivada.GetType());
object o = iderivada;
ClaseBase b = iderivada;
Console.WriteLine("object o = iderivada: Type is {0}", o.GetType());
Console.WriteLine("ibase b = iderivada: Type is {0}", b.GetType());
Console.ReadLine();
}
}
La reflexión puede emplearse para examinar los métodos, propiedades, ... de una clase:
using System;
using System.Reflection;
class ReflectApp
{
public class Test
{
private int n;
// Parámetros
if (ParInf.Length > 0) {
Console.WriteLine (" Parámetros: " );
foreach (ParameterInfo p in ParInf)
Console.WriteLine(" " + p.ParameterType
+ " " + p.Name);
}
}
Console.ReadLine ();
}
}
Atributos
Un atributo es información que se puede añadir a los metadatos de un módulo de código.
Los atributos nos permiten "decorar" un elemento de nuestro código con información adicional.
C# es un lenguaje imperativo, pero, como todos los lenguajes de esta categoría, contiene
algunos elementos declarativos. Por ejemplo, la accesibilidad de un método de una clase se
especifica mediante su declaración como public, protected, private o internal. C#
generaliza esta capacidad permitiendo a los programadores inventar nuevas formas de
información declarativa, anexarlas a distintas entidades del programa y recuperarlas en tiempo
de ejecución. Los programas especifican esta información declarativa adicional mediante la
definición y el uso de atributos.
Esta información puede ser referente tanto al propio módulo o el ensamblado al que
peretenezca, como a los tipos de datos definidos en él, sus miembros, los parámetros de sus
métodos, los bloques set y get de sus propiedades e indexadores o los bloques add y
remove de sus eventos. Se pueden emplear en ensamblados, módulos, tipos, miembros,
valores de retorno y parámetros.
Atributos predefinidos
Si bien el programador puede definir cuantos atributos considere necesarios, algunos atributos
ya están predefinidos en la plataforma .NET.
Atributo Descripción
Browsable Propiedades y eventos que deben mostrarse en el inspector de objetos.
Clases y estructuras que pueden "serializarse" (esto es, volcarse en algún
Serializable
dispositivo de salida, p.ej. disco), como en Java.
Obsolete El compilador se quejará si alguien los utiliza ( deprecated en Java).
ProgId COM Prog ID
Transaction Características transaccionales de una clase.
Observar como al marcar como obsoleta la clase A se genera un error al compilar el módulo ya
que se emplea en la línea comentada.
...
[Obsolete("Clase A desfasada. Usar B en su lugar")]
class A {
public void F() {}
}
class B {
public void F() {}
}
class SimpleAtrPredefApp
{
static void Main(string[] args)
{
A a = new A(); // Avisos
a.F();
...
}
}
Declarar una clase atributo
[AttributeUsage(AttributeTargets.All)]
public class HelpAttribute: Attribute
{
public string Topic = null;
private string url;
[Help("http://decsai.ugr.es/Clase1.htm")]
class Clase1
{
/* Bla, bla, bla... */
}
[Help("http://decsai.ugr.es/Clase2.htm", Topic="Atributos")]
class Clase2
{
/* Bla, bla, bla... */
}
En este ejemplo, el atributo HelpAttribute está asociado con las clases Clase1 y Clase2.
Nota: Por convención, todos los nombres de atributo finalizan con la palabra "Attribute"
para distinguirlos de otros elementos de .NET Framework. No obstante, no tiene que
especificar el sufijo de atributo cuando utiliza atributos en el código (véase el ejemplo).
Ejemplo 1
El siguiente ejemplo muestra la manera básica de utilizar la reflexión para obtener acceso a los
atributos:
class AtributosSimpleApp
{
static void Main(string[] args)
{
MemberInfo info1 = typeof(Clase1);
object[] attributes1 = info1.GetCustomAttributes(true);
for (int i = 0; i < attributes1.Length; i ++) {
System.Console.WriteLine(attributes1[i]);
}
MemberInfo info2 = typeof(Clase2);
object[] attributes2 = info2.GetCustomAttributes(true);
for (int i = 0; i < attributes2.Length; i ++) {
System.Console.WriteLine(attributes2[i]);
}
Console.ReadLine();
} // Main ()
} // class AtributosSimpleApp
Ejemplo 2
Atributos y reflexión
using System;
using System.Diagnostics;
using System.Reflection;
[AttributeUsage(AttributeTargets.All)]
public class IsTestedAttribute : Attribute {
public override string ToString() { return (" REVISADO"); }
}
[AttributeUsage(AttributeTargets.All)]
public class HelpAttribute: Attribute {
public string Topic = null;
private string url;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class CodeReviewAttribute: System.Attribute
{
public CodeReviewAttribute(string reviewer, string date) {
this.reviewer = reviewer;
this.date = date;
this.comment = "";
}
[IsTested]
public override string ToString()
{
return (this.c1c1.ToString() + this.c2c1.ToString());
}
}
[IsTested]
public char Met1Clase2 ()
{
return (this.c1c2[0]);
}
}
[IsTested]
public Clase3 (int n1) {
this.c1c3 = n1;
}
}
class Atributos1App
{
private static bool IsMemberTested (MemberInfo member)
{
foreach (object attr in member.GetCustomAttributes(true))
if (attr is IsTestedAttribute) return true;
return false;
}
Console.WriteLine(attribute);
}
}
t[0] = typeof(Clase1);
t[1] = typeof(Clase2);
t[2] = typeof(Clase3);
DumpAttributes(t[i]);
}
Console.ReadLine();
}
}
Programas en C#
Los espacios de nombres son mecanismos para controlar la visibilidad (ámbito) de los nombres
empleados e un programa. Su propósito es el de facilitar la combinación de los componentes
de un programa (que pueden provenir de varias fuentes) minimizando los conflictos entre
identificadores.
Por un lado estos espacios permiten tener más organizados los tipos de datos,
lo que facilita su localización. Así es como está organizada la BCL: todas las
clases más comúnmente usadas en cualquier aplicación pertenecen al
espacio de nombres llamado System, las de acceso a bases de datos en
System.Data, las de realización de operaciones de entrada/salida en
System.IO, etc.
Por otro lado, los espacios de nombres también permiten poder usar en un
mismo programa clases homónimas, siempre que pertenezcan a espacios
de nombres diferentes y queden perfectamente cualificadas.
El siguiente ejemplo trabaja con un espacio de nombres que incluye la declaración de un clase:
namespace Graficos2D
{
public class Point { ... }
}
Los componentes del espacio de nombres no son visibles directamente desde fuera del espacio
en el que están inmersos (al igual que los componentes de un struct, de una enumeración,
de una clase, ...) a no ser que se cualifiquen completamente:
namespace Graficos2D
{
public class Point { ... }
}
class MainClass
{
Graficos2D.Point p;
static void Main(string[] args)
{
...
}
}
La declaración
Point p;
produciría un error de compilación. En el siguiente ejemplo se trabaja con dos espacios de
nombres que incluyen una clase homónima (Point) en cada uno de ellos. Observe el uso
correcto de cada una de las dos clases cuando se cualifican completamente:
using System;
namespace Graficos2D
{
public class Point { ... }
}
namespace Graficos3D
{
public class Point { ... }
}
class MainClass
{
Graficos2D.Point p1;
Graficos3D.Point p2;
namespace N1
{
public class C1
{
public class C2 {}
}
namespace N2
{
public class C2 {}
}
}
class MainClass
{
N1.C1 o1;
N1.C1.C2 o2;
N1.N2.C2 o3;
La directiva using
La directiva using se utiliza para permitir el uso de tipos en un espacio de nombres, de modo
que no sea necesario especificar el uso de un tipo en ese espacio de nombres (directiva
using).
using System;
namespace N1
{
public class C1
{
public class C2 {}
}
namespace N2
{
public class C2 {}
}
}
namespace DemoNamespace
{
using N1;
class MainClass
{
C1 o1; // N1 es implícito (N1.C1)
N1.C1 o1_bis; // Tipo totalmente cualificado
La directiva using también puede usarse para crear un alias para un espacio de nombres
(alias using).
Uso de alias
using System;
namespace Graficos2D
{
public class Point { }
}
namespace Graficos3D
{
public class Point { }
}
namespace DemoNamespace
{
using Point2D = Graficos2D.Point;
using Point3D = Graficos3D.Point;
class MainClass
{
Point2D p1;
Point3D p2;
}
}
}
using System;
namespace Graficos2D
{
public class Point { }
public class Almacen {}
}
namespace DemoNamespace
{
using Graficos2D;
class MainClass
{
Point p1;
int[] Almacen = new int[20];
static void Main(string[] args)
{
}
}
}
Organización física de los tipos
Ambos son ficheros que contienen definiciones de tipos de datos. Se diferencian en que sólo
los primeros (.exe) disponen de un método especial que sirve de punto de entrada a partir del
que es posible ejecutar el código que contienen haciendo una llamada desde la línea de
órdenes del sistema operativo.
Los espacios de nombres son una construcción del lenguaje para abreviar nombres, mientras
que las referencias son las que especifican qué assembly utilizar.
Un ejercicio
Sobre un proyecto nuevo, trabajaremos con dos ficheros de código: uno contendrá el método
Main() y el otro un espacio de nombres con una clase que se empelará en Main().
Los objetos que deban eliminarse tras ser utilizados deberían implementar la interfaz
System.IDisposable (escribiendo en el método Dispose todo aquello que haya de
realizarse para liberar un objeto). El método Dispose siempre se invoca al terminar una
sentencia using:
struct MiStruct
{
private unsafe int * pX; // Campo de tipo puntero no
seguro
...
}
unsafe
{
// Instrucciones que usan punteros
}
En caso de que la compilación se vaya a realizar a través de Visual Studio .NET, la forma de
indicar que se desea compilar código inseguro es activando la casilla Proyecto | Propiedades
de (proyecto) | Propiedades de Configuración | Generar | Permitir bloques de código
no seguro | True.
class StackallocApp
{
public unsafe static void Main()
{
const int TAM = 10;
int * pt = stackalloc int[TAM];
Console.ReadLine ();
}
}
Para asegurarnos de que el recolector de basura no mueve nuestros datos tendremos que
utilizar la sentencia fixed. El recolector puede mover los datos de tipo referencia, por lo que
si un puntero contiene la dirección de un dato de tipo referencia podría apuntar a una dirección
incorrecta después de que el recolector de basura trabajara. Si un conjunto de instrucciones se
encierra en un bloque fixed se previene al recolector de basura para que no mueva el objeto
al que se referencia mientras dura el bloque fixed.
Esta capacidad tiene su coste: al emplear punteros, el código resultante es inseguro ya que
éste no se puede verificar. De modo que tendremos que extremar las precauciones si alguna
vez tenemos que usar esta capacidad del lenguaje C#.
Preprocesador
C# proporciona una serie de directivas de preprocesamiento con distintas funciones. Aunque
se le sigue llamando preprocesador (como en C o C++), el preprocesador no es independiente
del compilador y se han eliminado algunas directivas como #include (para mejorar los
tiempos de compilación, se utiliza el esquema de lenguajes como Java o Delphi en lugar de los
ficheros de cabecera típicos de C/C++) o las macros de #define (para mejorar la claridad del
código).
Directiva Descripción
Definición de símbolos para la compilación
#define, #undef
condicional.
#if, #elif, #else,
Compilación condicional.
#endif
#error, #warning Emisión de errores y avisos.
#region, #end Delimitación de regiones.
#line Especificación de números de línea.
Aserciones
Las aserciones nos permiten mejorar la calidad de nuestro código. Esencialmente, las
aserciones no son más que pruebas de unidad que están incluidas en el propio código fuente.
Las aserciones nos permiten comprobar precondiciones, postcondiciones e invariantes. Las
aserciones sólo se habilitan cuando se compila el código para depurarlo, de forma que su
correcto funcionamiento se compruebe continuamente. Cuando distribuyamos nuestras
aplicaciones, las aserciones se eliminan para no empeorar la eficiencia de nuestro código.
El método Assert() comprueba una condición y muestra un mensaje si ésta es falsa. Puede
emplearse cualquiera de estas versiones:
void DoSomething()
{
...
Assert((x == y), "X debería ser igual a Y");
...
}
}
Documentación
A los programadores no les suele gustar documentar código, por lo que resulta conveniente
suministrar un mecanismo sencillo que les permita mantener su documentación actualizada. Al
estilo de doxygen o Javadoc, el compilador de C# es capaz de generarla automáticamente a
partir de los comentarios que el progamador escriba en los ficheros de código fuente. Los
comentarios a partir de los cuales se genera la documentación se escriben en XML.
El hecho de que la documentación se genere a partir de los fuentes permite evitar que se
tenga que trabajar con dos tipos de documentos por separado (fuentes y documentación) que
deban actualizarse simultáneamente para evitar incosistencias entre ellos derivadas de que
evolucionen de manera separada ya sea por pereza o por error.
El compilador genera la documentación en XML con la idea de que sea fácilmente legible para
cualquier aplicación. Para facilitar su legibilidad a humanos bastaría añaderle una hoja de estilo
XSL o usar alguna aplicación específica encargada de leerla y mostrarla de una forma más
cómoda para humanos.
Los comentarios XML se denotan con una barra triple ( ///) y nos permiten generar la
documentación del código cuando compilamos con la opción /doc.
El formato de los comentarios viene definido en un esquema XML, si bien podemos añadir
nuestras propias etiquetas para personalizar la documentación de nuestras aplicaciones.
Algunas de las etiquetas predefinidas se verifican cuando generamos la documentación
(parámetros, excepciones, tipos...).
Estos comentarios han preceder las definiciones de los elementos a documentar. Estos
elementos sólo pueden ser definiciones de miembros, ya sean tipos de datos (que son
miembros de espacios de nombres) o miembros de tipos datos, y han de colocarse incluso
incluso antes que sus atributos.
<permission> Permisos.
using System;
namespace Geometria {
/// <summary>
/// Clase Punto.
/// </summary>
/// <remarks>
/// Caracteriza a los puntos de un espacio bidimensional.
/// Tiene múltiples aplicaciones....
/// </remarks>
class Punto {
/// <summary>
/// Campo que contiene la coordenada X de un punto
/// </summary>
/// <remarks>
/// Es de solo lectura
/// </remarks>
/// <summary>
/// Campo que contiene la coordenada Y de un punto
/// </summary>
/// <remarks>
/// Es de solo lectura
/// </remarks>
/// <summary>
/// Constructor de la clase
/// </summary>
/// <param name="x">Coordenada x</param>
/// <param name="y">Coordenada y</param>
class Cuadrado {
/// <summary>
/// Campo que contiene las coordenadas del vértice 1.
/// </summary>
///
protected Punto vertice1;
/// <summary>
/// Campo que contiene la longitud del lado.
/// </summary>
protected uint lado;
/// <summary>
/// Constructor de la clase.
/// Construye un cuadrado a partir del vértice 1 y de
/// la longitud del lado.
/// </summary>
/// <param name="vert1">Coordenada del vértice 1</param>
/// <param name="lado">Longitud del lado</param>
///
public Cuadrado(Punto vert1, uint lado) {
this.vertice1=vert1;
this.lado=lado;
}
/// <summary>
/// Constructor de la clase.
/// Construye un cuadrado a partir de los vértices 1 y 3.
/// </summary>
/// <param name="vert1">Coordenada del vértice 1</param>
/// <param name="vert3">Coordenada del vértice 3</param>
/// <remarks>
/// Habría que comprobar si las componentes del vértice 3
/// son mayores o menores que las del vértice1.
/// Vamos a presuponer que las componentes del vértice 3 son
/// siempre mayores que las del uno.
/// </remarks>
public Cuadrado(Punto vert1, Punto vert3) {
this.vertice1=vert1;
this.lado=(uint) Math.Abs(vert3.X-vert1.X);
}
/// <summary>
/// Propiedad que devuelve el punto que representa a
/// las coordenadas del vértice 1.
/// </summary>
public Punto Vertice1 {
get {
return this.vertice1;
}
}
/// <summary>
/// Propiedad que devuelve el punto que representa a
/// las coordenadas del vértice 2.
/// </summary>
public Punto Vertice2 {
get {
Punto p=new Punto(this.vertice1.X + this.lado,this.vertice1.Y);
return p;
}
}
......
namespace PruebaGeometria {
using Geometria;
class GeometriaApp {
....
Conceptos básicos
Objetos, instancias y clases: Un objeto es una estructura de datos en
tiempo de ejecución, formada por uno o más valores (campos) y que sirve
como representación de un objeto abstracto. Todo objeto es instancia de
una clase. Una clase es un tipo abstracto de datos implementado total o
parcialmente, que encapsula datos y operaciones. Las clases sirven de
módulos y de tipos (o patrones de tipos si son genéricas).
Módulo: Unidad lógica que permite descomponer el software. En
programación orientada a objetos, las clases proporcionan la forma básica
de módulo. Para facilitar el desarrollo de software y su posible reutilización,
las dependencias entre módulos deberían reducirse al máximo para
conseguir sistemas débilmente acoplados.
Tipo: Cada objeto tiene un tipo, que describe un conjunto de operaciones con
las que están equipados todos los objetos de una misma clase.
Interfaz: Contrato perfectamente definido que especifica completamente las
condiciones precisas que gobiernan las relaciones entre una clase
proveedora y sus clientes (rutinas exportadas). Además, es deseable
conocer las precondiciones, postcondiciones e invariantes que sean
aplicables.
Identidad: Cada objeto (instancia de una clase) tiene una identidad única,
independientemente de su contenido actual (los datos almacenados en sus
campos).
Encapsulación (u ocultación de información): Capacidad de evitar que
ciertos aspectos sean visibles desde el exterior. De esta forma, se ocultan
detalles de implementación y el usuario puede emplear objetos sin tener
que conocer su estructura interna.
Herencia: Los tipos se organizan de forma jerárquica (clases base y
derivadas, superclases y subclases). La herencia proporciona un mecanismo
simple mediante el cual se pueden definir unos tipos en función de otros, a
los que añade sus características propias. Hay que distinguir entre herencia
de interfaz y herencia de implementación (que aumenta el acoplamiento
entre los módulos de un programa).
Polimorfismo: Capacidad de usar un objeto sin saber su tipo exacto.
Formalmente, el polimorfismo es la capacidad de que un elemento de
código pueda denotar, en tiempo de ejecución, objetos de dos o más tipos
distintos.
Referencias
Bertrand Meyer: "Construcción de software orientado a objetos", [2ª ed.], Prentice Hall, 1999,
ISBN 84-8322-040-7.
La biblioteca de clases de la plataforma
.NET
System
Colecciones
Entrada / Salida
Esta sección describe algunas clases e interfaces de la plataforma .NET con el objetivo de que
el lector sea capaz de utilizarlas al desarrollar sus propios programas.
Antes de que existiese la plataforma .NET, cada lenguaje de programación tenía su propia
biblioteca de clases, lo que provocaba que no todos los lenguajes dispusiesen de la misma
funcionalidad (p.ej. algunos APIs no estaban soportados en Visual Basic, por lo que había que
recurrir a C/C++ para desarrollar determinados tipos de aplicaciones). Además, la
funcionalidad proporcionada por distintas clases estaba repartida en componentes COM/COM+,
controles ActiveX, DLLs del sistema... lo cual dificultaba su organización (además de hacer casi
imposible la implementación de extensiones de las clases disponibles).
La plataforma .NET incluye una colección de clases bien organizada cuya parte
independiente del sistema operativo ha sido propuesta para su estandarización
(http://msdn.microsoft.com/net/ecma/).
La biblioteca de clases de la plataforma .NET integra todas las tecnologías Windows
en un marco único para todos los lenguajes de programación (Windows Forms, GDI+,
Web Forms, Web Services, impresión, redes...).
La biblioteca de clases .NET proporciona un modelo orientado a objetos que sustituye
a los componentes COM.
System
El espacio de nombres System incluye la clase base para todos los objetos ( System.Object),
los tipos de datos primitivos y algunos no tan primitivos (cadenas, textos, fechas, horas y
calendarios), además de un conjunto de interfaces estándar y soporte para la E/S a través de
consola.
System.Object
La clase System.Object es la clase base para todos los tipos de la plataforma .NET.
Cualquier tipo hereda implícitamente de esta clase, de forma que se unifica el sistema de
tipos. Esto facilita que las colecciones se puedan usar para almacenar cualquier cosa y es
menos propenso a errores que el tipo VARIANT utilizado en COM (al ser un sistema
fuertemente tipificado).
Métodos de System.Object
...
Se elimina la distinción entre los tipos primitivos del lenguaje y los demás objetos (al estilo de
Smalltalk). Los tipos primitivos son comunes para toda la plataforma .NET, si bien se muestran
en cada lenguaje según su sintaxis:
Cadenas de caracteres
Comparación de cadenas
if (s1 == s2)
Console.WriteLine("OK");
else
Console.WriteLine("Esto no debería pasar");
return 0
}
Fechas
System.DateTime permite representar fechas (100 d.C. - 9999 d.C.) y realizar
operaciones aritméticas con ellas, así como darles formato y leerlas de una cadena en
función de la configuración local.
System.TimeSpan sirve para trabajar con duraciones de tiempo (que se pueden
expersar en distintas unidades).
System.TimeZone permite trabajar con husos horarios.
Consola
Clase Función
System.URI Identificadores universales de recursos
System.Random Generador de números aleatorios
System.Convert Conversiones de tipos básicos
Interfaces de System
IFormattable
El interfaz IFormattable nos permite dar formato a la representación del valor de un objeto:
interface IFormattable
{
String Format(String format, IServiceObjectProvider sop);
}
IDisposable
Entre sus cualidades más destacadas destaca el hecho de que los arrays pueden ordenarse
(siempre y cuando se comparen objetos que implementen el interfaz IComparable o se
indique un comparador IComparer). Además, si el array está ordenado, se pueden realizar
búsquedas binarias en él.
Interfaces
Aparte de los arrays, la plataforma .NET también suministra una serie de interfaces estándar
para la implementación de colecciones de objetos de distintos tipos (similar a las colecciones
de Java).
IEnumerator e = st.GetEnumerator();
while (e.MoveNext()) {
// Se lee un elemento de la colección
ItemType it = (ItemType) e.Current;
// Se emplea el elemento para hacer lo que haga
falta
Console.WriteLine(it);
}
ICollection
IList
IDictionary
Clases
Aparte de los interfaces estándar que sirven como base para la implementación de colecciones,
la biblioteca de clases de la plataforma .NET también incluye algunas colecciones ya
implementadas:
ArrayList
using System;
using System.Collections;
// Inicialización
myAL.Add("Hello");
myAL.Add("World");
myAL.Add("!");
// Visualización
Console.WriteLine( "myAL" );
Console.WriteLine( "\tCount: {0}", myAL.Count );
Console.WriteLine( "\tCapacity: {0}", myAL.Capacity );
Console.Write( "\tValues:" );
PrintValues( myAL );
}
while ( myEnumerator.MoveNext() )
Console.Write("\t{0}", myEnumerator.Current );
Console.WriteLine();
}
}
BitArray
HashTable
SortedList
La clase System.Collections.Stack implementa una pila LIFO con sus métodos Push() y
Pop(). Al implementar el interfaz IEnumerable, podemos enumerar sus elementos:
using System;
using System.Collections;
myStack.Push("Hello");
myStack.Push("World");
myStack.Push("!");
Console.WriteLine( "myStack" );
Console.WriteLine( "\tCount: {0}", myStack.Count );
Console.Write( "\tValues:" );
PrintValues( myStack );
Console.Write( "\tLIFO:" );
EmptyStack ( myStack );
}
while ( myEnumerator.MoveNext() )
Console.Write( "\t{0}", myEnumerator.Current );
Console.WriteLine();
}
while (value!=null) {
Console.Write ("\t{0}", value);
value = myStack.Pop();
}
Console.WriteLine();
}
}
Queue
using System;
using System.Collections;
public class SampleQueue
{
public static void Main()
{
Queue myQ = new Queue();
myQ.Enqueue("Hello");
myQ.Enqueue("World");
myQ.Enqueue("!");
Console.WriteLine( "myQ" );
Console.WriteLine( "\tCount: {0}", myQ.Count );
Console.Write( "\tValues:" );
PrintValues( myQ );
Console.Write( "\tFIFO:" );
EmptyQueue (myQ);
}
while ( myEnumerator.MoveNext() )
Console.Write( "\t{0}", myEnumerator.Current );
Console.WriteLine();
}
while (value!=null) {
Console.Write ("\t{0}", value);
value = myQ.Dequeue();
}
Console.WriteLine();
}
}
Entrada / Salida
Ficheros y directorios
La biblioteca de clases de la plataforma .NET proporciona una serie de clases que nos permiten
trabajar con el sistema de archivos:
Una vez que tenemos un fichero con el que trabajar, utilizamos uno de sus métodos Open...
para poder acceder y modificar su contenido: Open(), OpenRead(), OpenWrite(),
OpenText(). Cualquiera de los métodos anteriores devuelve una instancia de
System.IO.Stream.
Streams
Lectura
Console.WriteLine ("*****************************");
do {
line = stream.ReadLine();
Console.WriteLine (line);
} while (line != null);
stream.Close();
Console.WriteLine ("*****************************");
Console.ReadLine();
}
Escritura
streamI.Close();
streamO.Close();
}
System.Net
El espacio de nombres System.Net contiene todas las clases relativas al uso de protocolos de
red para transmitir datos. Proporciona todo lo necesario para utilizar los protocolos de red IP
(sockets) e IPX, así como otros protocolos de aplicación (p.ej. HTTP, incluidos sus mecanismos
de autentificación [Basic, Digest, NTLM Challenge/Reponse] y el uso de cookies).
HTTP
HttpWebRequest HttpWReq =
(HttpWebRequest)
WebRequestFactory.Create("http://elvex.ugr.es");
HttpWebResponse HttpWResp =
(HttpWebResponse)HttpWReq.GetResponse();
SMTP
Serialización
class SerializeExample
{
public static void Main(String[] args)
{
ArrayList l = new ArrayList();
FileStream s = File.Create("foo.bin");
BinaryFormatter b = new BinaryFormatter();
b.Serialize(s, l);
}
}
using System;
using System.IO;
using System.Collections;
using System.Serialization;
using System.Serialization.Formatters.Binary;
class DeserializeExample
{
public static void Main(String[] args)
{
FileStream s = File.Open("foo.bin");
BinaryFormatter b = new BinaryFormatter();
// Métodos
object Deserialize(Stream serializationStream);
void Serialize(Stream serializationStream, object graph);
}
Acceso a bases de datos con ADO.NET
Bases de datos
Cualquier aplicación de interés requiere el almacenamiento y posterior recuperación de los
datos con los que trabaje (pedidos en aplicaciones de comercio electrónico, datos de personal
para las aplicaciones de recursos humanos, datos de clientes en sistemas CRM, etc.). Los
sistemas de gestión de bases de datos (DBMSs) nos permiten almacenar, visualizar y modificar
datos, así como hacer copias de seguridad y mantener la integridad de los datos,
proporcionando una serie de funciones que facilitan el desarrollo de nuevas aplicaciones.
Desde un punto de vista intuitivo, una base de datos no es más que un fondo común de
información almacenada en una computadora para que cualquier persona o programa
autorizado pueda acceder a ella, independientemente de su lugar de procedencia y del uso que
haga de ella. Algo más formalemente, una base de datos es un conjunto de datos comunes a
un "proyecto" que se almacenan sin redundancia para ser útiles en diferentes aplicaciones.
El Sistema de Gestión de Bases de Datos (DBMS) es el software con capacidad para definir,
mantener y utilizar una base de datos. Un sistema de gestión de bases de datos debe permitir
definir estructuras de almacenamiento, así como acceder a los datos de forma eficiente y
segura. Ejemplos: Oracle, IBM DB2, Microsoft SQL Server, Interbase...
En una base de datos, los datos se organizan independientemente de las aplicaciones que los
vayan a usar (independencia lógica) y de los ficheros en los que vayan a almacenarse
(independencia física). Además, los datos deben ser accesibles a los usuarios de la manera
más amigable posible, generalmente mediante lenguajes de consulta como SQL o Query-by-
example. Por otro lado, es esencial que no exista redundancia (esto es, los datos no deben
estar duplicados) para evitar problemas de consistencia e integridad.
Se pueden estrablecer relaciones entre las tablas de una base de datos relacional mediante el
uso de claves primarias y claves externas (p.ej. cada libro tiene, al menos, un autor).
Lenguaje estándar para acceder a una base de datos relacional, estandarizado por el American
National Standards Institute (ANSI): SQL-92. En gran parte, los distintos DBMS utilizan todos
el mismo SQL, si bien cada vendedor le ha añadido sus propias extensiones. El lenguaje SQL
se divide en:
ADO se diseñó para su uso en arquitecturas cliente/servidor con bases de datos relacionales
(no jerárquicas, como es el caso de XML). Su diseño no está demasiado bien factorizado (ya
que existen muchas formas de hacer las cosas y algunos objetos acaparan demasiadas
funciones) y ADO no estaba pensado para arquitecturas multicapa en entornos distribuidos.
ADO .NET es una colección de clases, interfaces, estructuras y tipos enumerados que
permiten acceder a los datos almacenados en una base de datos desde la plataforma .NET. Si
bien se puede considerar una versión mejorada de ADO, no comparte con éste su jerarquía de
clases (aunque sí su funcionalidad).
ADO .NET combina las capas ADO y OLE DB en una única capa de proveedores (managed
providers). Cada proveedor contiene un conjunto de clases que implementan interfaces
comunes para permitir el acceso uniforme a distintas fuentes de datos. Ejemplos: ADO
Managed Provider (da acceso a cualquier fuente de datos OLE DB), SQL Server Managed
Provider (específico para el DBMS de Microsoft), Exchange Managed Provider (datos
almacenados con Microsoft Exchange)...
ADO .NET usa XML. De hecho, los conjuntos de datos se almacenan internamente en XML, en
vez de almacenarse en binario como sucedía en ADO. Al estar los datos almacenados en XML,
se simplifica el acceso a los datos a través de HTTP (algo que ocasiona problemas en ADO si
los datos tienen que pasar cortafuegos). Por otro lado, se simplifica la comunicación entre
aplicaciones al ser XML un formato estándar (p.ej. comunicación con applets Java).
Con ADO .NET se puede acceder a los datos de dos formas distintas:
Como los conjuntos de datos se almacenan en memoria y trabaja con ellos de forma
desconectada, cuando hagamos cambios sobre ellos (inserciones, borrados o actualizaciones)
debemos actualizar el contenido de la base de datos llamando al método Update del
DataAdapter y, posteriormente, confirmar los cambios realizados en el DataSet (con
AcceptChanges) o deshacerlos (con RejectChanges).
Clases ADO.NET
ADO .NET define una serie de interfaces que proporcionan la funcionalidad básica común a las
distintas fuentes de datos accesibles a través de ADO .NET. La implementación de estos
interfaces por parte de cada proveedor proporciona acceso a un tipo concreto de fuentes de
datos y puede incluir propiedades y métodos adicionales.
Interfaz IDbConnection
Establece una sesión con una fuente de datos. Permite abrir y cerrar conexiones, así como
comenzar transacciones (que se finalizan con los métodos Commit y Rollback de
IDbTransaction. Las clases SqlDbConnection y OleDbConnection implementan el
interfaz de IDbConnection.
Interfaz IDbCommand
Representa una sentencia que se envía a una fuente de datos (usualmente en SQL, aunque no
necesariemente). Las clases SqlDbCommand y OleDbCommand implementan el interfaz de
IDbCommand.
param.Value = ...
connection.Open();
insertCommand.ExecuteNonQuery();
connection.Close();
command.CommandType = CommandType.StoredProcedure;
SqlParameter in = command.Parameters.Add (
new SqlParameter("@DepartmentName", SqlDbType.VarChar,
100));
in.Value = ...
connection.Open();
insertCommand.ExecuteNonQuery();
connection.Close();
// SQL Server
// ----------
Interfaz IDataReader
Proporciona acceso secuencial de sólo lectura a una fuente de datos. Las clases
SqlDataReader y OleDbDataReader implementan el interfaz de IDataReader. Al utilizar
un objeto IDataReader, las operaciones sobre la conexión IDbConnection quedan
deshabilitadas hasta que se cierre el objeto IDataReader.
string connectionString = "Provider=SQLOLEDB.1;" +
"User ID=sa;Initial Catalog=Northwind;" +
"Data Source=MYSERVER";
while (myReader.Read()) {
Console.WriteLine(myReader.GetString(0));
}
myReader.Close();
connection.Close();
Clase DataSet
connection.Open();
myDataAdapter.Fill(myDataSet);
conn.Close();
Clase DataTable
Representa una tabla en memoria (Columns & Rows) cuyo esquema viene definido por su
colección de columnas Columns. La integridad de los datos se conserva gracias a objetos que
representan restricciones (Constraint) y dispone de eventos públicos que se producen al
realizar operaciones sobre la tabla (p.ej. modificación o eliminación de filas).
Clase DataColumn
Define el tipo de una columna de una tabla (vía su propiedad DataType) e incluye las
restricciones (Constraints) y las relaciones (Relations) que afectan a la columna.
Además, posee propiedades útiles como AllowNull, Unique o ReadOnly.
Clase DataRow
Clase DataRelation
dataset.Relations.Add (bookauth);
// Inserción de datos
// Commit
dataset.AcceptChanges();
Transacciones en ADO.NET
Las transacciones son conjuntos de operaciones que han de efectuarse de forma atómica. La
acidez de una transacción hace referencia a sus propiedades deseables: atomicidad,
consistencia, aislamiento y durabilidad (ACID = Atomicity, Consistency, Isolation, Durability).
En ADO.NET, los límites de las transacciones se indican manualmente. Los objetos de la clase
Connection tienen un método BeginTransaction que devuelve una transacción (objeto de
tipo Transaction). La transacción finaliza cuando se llama al método Commit o Rollback
del objeto Transaction devuelto por BeginTransaction.
Data Binding
Éste es el nombre por el que se conoce el mecanismo que nos permite asociar el contenido de
un conjunto de datos a los controles de la interfaz de nuestra aplicación, algo que facilitan los
entornos de programación visual (como es el caso del Visual Studio .NET).
NOTA: Cuando los conjuntos de datos sean grandes, es recomendable utilizar ADO.NET con
paginación para no tener que leer todos los datos de la base de datos (véase "ADO.NET data
paging"). En situaciones como ésa, también suele ser recomendable añadir capacidades de
búsqueda a nuestras aplicaciones (véase el método Find).
XML
¿Qué es XML?
Sintaxis de XML
Espacios de nombres
Tecnologías relacionadas
¿Qué es XML?
XML es el acrónimo de eXtensible Markup Language, un formato estándar del World Wide
Web Consortium (W3C) diseñado a partir de SGML para representar datos estructurados de
forma jerárquica (en un árbol).
Los documentos XML incluyen una serie de etiquetas que permiten crear documentos
autocontenidos, en los que los datos van siempre acompañados de sus metadatos
correspondientes.
XML no es, como su nombre puede sugerir, un lenguaje de marcado: XML es un metalenguaje
que permite definir lenguajes de marcado adecuados a usos específicos.
Aunque a primera vista un docuento XML puede parecer similar a HTML hay una diferencia
fundamental: un documento XML contiene, exclusivamente, datos que se autodefinen. Un
documento HTML contiene datos "mal" definidos, mezclados con elementos de formato. En
XML, sin embargo, se separa el contenido de la presentación de forma total.
Representación de datos en formato ASCII, antes de que existiese XML (más concretamente,
en formato CSV, comma-separated values):
"PO-1234","CUST001","X9876","5","14.98"
<pedido>
<id>PO-1234</id>
<cliente>CUST001</cliente>
<producto>X9876</producto>
<cantidad>5</cantidad>
<precio>14.98</precio>
</pedido>
Sintaxis de XML
Antes de ver los distintos componentes que pueden aparecer en un documento XML, hay que
resaltar que XML es sensible a mayúsculas. A diferencia de HTML, <tag> y <TAG>
representan cosas diferentes.
Elementos
Los elementos XML están delimitados por etiquetas de comienzo y fin entre las que se escribe
su contenido:
<tag_name />
Atributos
Los distintos elementos de un documento XML pueden incluir atributos que describen al
elemento en cuestión.
Dichos atributos han de aparecer en la etiqueta de comienzo del elemento y el valor del
atributo debe especificarse entre comillas dobles (") o simples ('):
Instrucciones de procesamiento
En su prólogo, un documento XML puede incluir una serie de instrucciones de
procesamiento, delimitadas por: <? ...
?>, en las que se puede indicar el sistema de codificación empleado (Unicode por defecto),
especificar la hoja de estilo XSLT que se empleará para visualizar el documento, declarar
espacios de nombres, definir el esquema del documento, etcétera.
Sólo es obligatorio especificar que se trata de un documento XML usando la instrucción xml:
<?xml version="1.0"?>
Esta misma instrucción de procesamiento es la que se utiliza para especificar la codificación del
documento:
<root>
<element1>
<subelement1 />
<subelement2 />
</element1>
<element2>...</element2>
<element3>...</element3>
<element4>...</element4>
</root>
Entidades
Entidad Sustitución
< <
> >
& &
' '
" "
XML permite que se puedan definir nuevas entidades. Algunas (parsed entities) pueden
contener texto y etiquetas XML, mientras que otras (unparsed entities) sirven para almacenar
cualquier tipo de datos (imágenes, sonidos...).
Entidades predefinidas
<!ENTITY lt "&#60;">
<!ENTITY gt ">">
<!ENTITY amp "&#38;">
<!ENTITY apos "'">
<!ENTITY quot """>
Secciones CDATA, que sirven para incluir cualquier cosa (contenido textual) en
el documento XML y vienen delimitadas por las construcciones <![CDATA[
y ]]> (como <PRE> ... </PRE> en HTML).
<ejemplo>
<HTML>
<HEAD>
<TITLE>Rock & Roll</TITLE>
</HEAD>
</HTML>
</ejemplo>
<!-- Este documento XML usa CDATA -->
<ejemplo>
<![CDATA[
<HTML>
<HEAD>
<TITLE>Rock & Roll</TITLE>
</HEAD>
</HTML>
]]>
</ejemplo>
Documentos XML bien formados
Un documento bien formado en XML tiene que reunir las siguientes cualidades:
Debe haber un y sólo un elemento raíz.
Los subelementos deben estar adecuadamente anidados. Esto es, un
elemento ha de terminar con la misma etiqueta con la que comenzó.
Los atributos son opcionales (y se definen en un esquema que también es
opcional).
Los valores de los atributos han de estar delimitados por comillas dobles (") o
comillas simples (').
Las instrucciones de procesamiento son opcionales.
XML es sensible a mayúsculas y minúsculas. Es decir, <tag> y <TAG> no
hacen referencia al mismo tipo de elemento.
Teniendo en cuenta las estrictas reglas anteriores, el siguiente documento XML no es válido
porque sus elementos no están anidados correctamente:
Sintaxis
xmlns:prefijo = URI
En la declaración anterior:
xmlns:bk = "http://www.example.com/bookinfo/"
xmlns:bk = "urn:mybookstuff.org:bookinfo"
Ejemplos de uso de namespaces
<libro xmlns:bk="http://www.bookstuff.org/bookinfo">
<bk:titulo>All About XML</bk:titulo>
<bk:autor>Joe Developer</bk:autor>
<bk:precio currency='US Dollar'>19.99</bk:precio>
</libro>
<bk:libro xmlns:bk="http://www.bookstuff.org/bookinfo"
xmlns:money="urn:finance:money">
<bk:titulo>All About XML</bk:titulo>
<bk:autor>Joe Developer</bk:autor>
<bk:precio money:currency='US Dollar'>19.99</bk:precio>
</bk:libro>
Al combinar documentos XML de distintas fuentes, se pueden producir conflictos entre los
nombres de los elementos y/o atributos.
Supongamos que disponemos del siguiente fichero XML con nuestra colección de libros:
libros.xml
<?xml version="1.0"?>
<biblioteca>
<item estado="disponible">
<titulo>The Adventures of Huckleberry Finn</titulo>
<autor>Mark Twain</autor>
<precio>$5.49</precio>
</item>
<item estado="prestado">
<titulo>Leaves of Grass</titulo>
<autor>Walt Whitman</autor>
<precio>$7.75</precio>
</item>
<item estado="prestado">
<titulo>The Legend of Sleepy Hollow</titulo>
<autor>Washington Irving</autor>
<precio>$2.95</precio>
</item>
<item estado="disponible">
<titulo>The Marble Faun</titulo>
<autor>Nathaniel Hawthorne</autor>
<precio>$10.95</precio>
</item>
</biblioteca>
También disponemos del siguiente documento XML con nuestra colección de discos:
discos.xml
<?xml version="1.0"?>
<discoteca>
<item>
<titulo>Violin Concerto in D</titulo>
<compositor>Beethoven</compositor>
<precio>$14.95</precio>
</item>
<item>
<titulo>Violin Concertos Numbers 1, 2, and 3</titulo>
<compositor>Mozart</compositor>
<precio>$16.49</precio>
</item>
</discoteca>
Queremos combinar estos documentos en uno solo y, además, queremos gestionarlo con una
única aplicación. El problema surge al haber elementos repetidos (con el mismo nombre). Por
ejemplo, ¿cómo hacer una lista de todos los libros? ¿cómo calcular e precio medio de los
discos?
El mecanismo de los espacios de nombres facilita esta tarea: basta con definir un espacio de
nombres para diferenciar cada elemento. En el documento combinado, cada elemento de un
libro se asigna al espacio de nombres libro (libro:item, libro:titulo, libro:autor y libro:precio)
y cada uno de los elementos de un disco se asigna al espacio de nombres cd (cd:item,
cd:titulo, cd:compositor y cd:precio).
biblioteca.xml
<?xml version="1.0"?>
<biblioteca
xmlns:libro="http://csharp.ikor.org/libros"
xmlns:cd="http://csharp.ikor.org/discos">
<libro:item estado="disponible">
<libro:titulo>
The Adventures of Huckleberry Finn
</libro:titulo>
<libro:autor>Mark Twain</libro:autor>
<libro:precio>$5.49</libro:precio>
</libro:item>
<cd:item>
<cd:titulo>Violin Concerto in D</cd:titulo>
<cd:compositor>Beethoven</cd:compositor>
<cd:precio>$14.95</cd:precio>
</cd:item>
<libro:item estado="prestado">
<libro:titulo>Leaves of Grass</libro:titulo>
<libro:autor>Walt Whitman</libro:autor>
<libro:precio>$7.75</libro:precio>
</libro:item>
<cd:item>
<cd:titulo>
Violin Concertos Numbers 1, 2, and 3
</cd:titulo>
<cd:compositor>Mozart</cd:compositor>
<cd:precio>$16.49</cd:precio>
</cd:item>
<libro:item estado="prestado">
<libro:titulo>
The Legend of Sleepy Hollow
</libro:titulo>
<libro:autor>Washington Irving</libro:autor>
<libro:precio>$2.95</libro:precio>
</libro:item>
<libro:item estado="disponible">
<libro:titulo>The Marble Faun</libro:titulo>
<libro:autor>Nathaniel Hawthorne</libro:autor>
<libro:precio>$10.95</libro:precio>
</libro:item>
</biblioteca>
<libro xmlns="http://csharp.ikor.org/libros">
<titulo>All About XML</titulo>
<autor>Joe Developer</autor>
</libro>
En esta situación, todos los elementos que aparezcan sin prefijo harán referencia al espacio de
nombres por defecto (y no hará falta repetir el prefijo para los elementos del espacio de
nombres por defecto).
default.xml
<?xml version="1.0"?>
<biblioteca
xmlns="http://csharp.ikor.org/libros"
xmlns:cd="http://csharp.ikor.org/discos">
<item estado="disponible">
<titulo>
The Adventures of Huckleberry Finn
</titulo>
<autor>Mark Twain</autor>
<precio>$5.49</precio>
</item>
<cd:item>
<cd:titulo>Violin Concerto in D</cd:titulo>
<cd:compositor>Beethoven</cd:compositor>
<cd:precio>$14.95</cd:precio>
</cd:item>
<item estado="prestado">
<titulo>Leaves of Grass</titulo>
<autor>Walt Whitman</autor>
<precio>$7.75</precio>
</item>
<cd:item>
<cd:titulo>
Violin Concertos Numbers 1, 2, and 3
</cd:titulo>
<cd:compositor>Mozart</cd:compositor>
<cd:precio>$16.49</cd:precio>
</cd:item>
<item estado="prestado">
<titulo>
The Legend of Sleepy Hollow
</titulo>
<autor>Washington Irving</autor>
<precio>$2.95</precio>
</item>
<item estado="disponible">
<titulo>The Marble Faun</titulo>
<autor>Nathaniel Hawthorne</autor>
<precio>$10.95</precio>
</item>
</biblioteca>
Los elementos no cualificados (esto es, sin prefijo relativo a un namespace) se consideran
pertenecientes al namespace por defecto más interno. En el siguiente ejemplo, libro,
titulo y autor corresponden al espacio de nombres por defecto (el de libro), mientras que
editorial y nombre pertenecen al espacio de nombres más interno
(urn:publishers:pubinfo):
<libro xmlns="http://csharp.ikor.org/libros">
<titulo>All About XML</titulo>
<autor>Joe Developer</autor>
<editorial xmlns="urn:publishers:pubinfo">
<nombre>Microsoft Press</nombre>
</editorial>
</libro>
A diferencia de los elementos (que, salvo que se indique lo contrario, pertenecen al espacio de
nombres por defecto), los atributos NO pertenecen a ningún espacio de nombres, incluso
aunque exista un espacio de nombres por defecto.
Tecnologías relacionadas
XML en sí es bastante simple. Sin embargo, existen múltiples tecnologías relacionadas cuyo
aprendizaje requiere algo más de esfuerzo:
eXtensible Markup
XML Definición de documentos XML
Language
Definición del esquema de
Document Type
DTD documentos XML (en un formato
Definition
distinto a XML)
Definición del esquema de
XSD XML Schema
documentos XML (en XML)
Precursor de XML Schema
XDR XML Data Reduced
(Microsoft)
eXtensible Stylesheet Definición de hojas de estilo (XSLT
XSL
Language + XSL-FO)
XSLT XSL Transformations Transformación de documentos XML
XSL Formatting Descripción del layout de un
XSL-FO
Objects documento
Acceso a partes de un documento
XPath XML Path language
XML (usado en XSLT y XPointer)
Acceso a la estructura interna de un
XPointer XML Pointer language
documento XML
Descripción de enlaces entre
XLink XML Linking language
documentos XML
Mecanismo para hacer consultas en
XQuery XML Query language
documentos XML
XMLEnc XML Encryption Criptografía para documentos XML
XMLDSig XML-Signature Firmas digitales en XML
XML Key
XKMS Gestión de claves en XML
Management
Document Object API para crear, acceder y modificar
DOM
Model documentos XML
API para trabajar con documentos
SAX Simple API for XML
XML
Versión de HTML compatible con
XHTML XML HTML
XML
XForms XML Forms Formularios web en XML
Data Inclusión de datos XML en un
Island documento HTML
Data Generación automática de HTML a
Binding partir de documentos XML
Esquemas XML
Un esquema XML es, básicamente, un conjunto de reglas predefinidas que describe una clase
de documentos XML. Un esquema define los elementos que pueden aparecer en un documento
XML, así como los atributos que pueden asociarse a éstos. También define información
estructural tal como enumerar los descendientes de un elemento, la secuencia en que pueden
aparecer, sus tipos, etc. En definitiva, un esquema permite especificar el formato correcto de
un documento XML.
Publicar cómo se han de construir documentos XML correctos (de forma similar a
como se publica la interfaz de un componente software) y,
Permitir la validación de un documento conforme a un esquema particular (para, por
ejemplo, comprobar que los datos que nos llegan están en el formato correcto). Se habla
entonces de que el documento XML es válido respecto a un esquema, lo que no debe
confundirse con estar bien formado.
Los esquemas XML y las definiciones DTD suelen especificarse en ficheros independientes, lo
que facilita la tarea de mantenimiento ya que un solo fichero (de esquema o DTD) puede servir
de referencia a muchos ficheros de datos XML.
<?xml version="1.0"?>
<!DOCTYPE Libro SYSTEM "libro.dtd">
.....
SYSTEM sirve para DTDs "personales". Se puede espicificar un fichero local o un fichero
accesible a través de una URL. Se puede especificar una DTD pública con PUBLIC, en la que
queda reflejado el propietario de la misma, una descripción y el idioma.
<?xml version="1.0"?>
<!ELEMENT Libro
(Titulo, Catalogo:Seccion, Catalogo:SubSeccion,
Contenido, Compra, Copyright)>
<!ATTLIST Libro
xmlns CDATA #REQUIRED
xmlns:Catalogo CDATA #REQUIRED
>
<!ELEMENT Titulo (#PCDATA)>
<!ELEMENT Catalogo:Seccion (#PCDATA)>
<!ELEMENT Catalogo:SubSeccion (#PCDATA)>
<!ELEMENT Contenido ((Capitulo+)|(Capitulo+, Separacion?)+)>
<!ELEMENT Capitulo (Tema, Seccion+)>
<!ATTLIST Capitulo
materia (XML|Java) "Java"
>
<!ELEMENT Tema (#PCDATA)>
<!ELEMENT Seccion (#PCDATA)>
<!ATTLIST Seccion
apartados CDATA #REQUIRED
dificil (si|no) "no"
>
<!ELEMENT Separacion EMPTY>
<!ELEMENT Compra (#PCDATA)>
<!ELEMENT Copyright (#PCDATA)>
Elementos
Los elementos permitidos se especifican con ELEMENT, seguido del nombre y el tipo del
elemento. Los elementos que se pueden anidar dentro de otros se especifican entre paréntesis
y separados por comas. Importa el orden. El tipo menos restrictivo es ANY, que permite
cualquier contenido para un elemento. Para datos de tipo texto, se usa #PCDATA. Para
elementos vacíos, EMPTY.
Atributos
Los atributos permitidos para un elemento se especifican con ATTLIST y el nombre del
elemento seguido de los nombres de los atributos, con un tipo y modificador obligatorios. El
tipo del atributo puede ser CDATA para cualquier valor, o una enumeración de los valores
permitidos.
Otros posibles tipos son: NMTOKEN para restringir el valor a un nombre XML válido (es decir,
que empiece con una letra o guión de subrayado y contenga sólo letras, números, guiones de
subrayado, guiones y puntos, sin espacios) ID, además de las restricciones que impone
NMTOKEN, impone que el valor sea único en todo el documento. El modificador puede ser
#REQUIRED para atributos obligatorios, #IMPLIED para opcionales, o #FIXED valor_fijo
para valores fijos. También puede ser un valor por defecto.
Sintaxis de XML Schema
El siguiente ejemplo muestra un sencillo esquema XML que podría ser útil para gestionar los
productos existentes en un almacén:
<xsd:complexType>
<xsd:sequence>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>
La etiqueta xsd:schema del documento XML anterior está definida en el estándar XSD y es la
que nos permite definir esquemas de acuerdo con el estándar del W3C. El atributo
targetNamespace nos permite asociar el esquema al espacio de nombres indicado (para
diferenciarlo de otros esquemas stock).
Los elementos se utilizan para especificar las etiquetas válidas en un documento XML (name) y
su tipo (type), tal como aparece en el ejemplo anterior del almacén. Además, el orden en que
aparecen los elementos en el esquema XML determina el orden en que han de aparecen dentro
de un documento XML que se ajuste al esquema.
La siguiente tabla muestra algunos de los tipos permitidos y su equivalencia con los tipos de la
plataforma .NET:
Por ejemplo, podemos definir un tipo de dato que sólo permita almacenar valores enteros
entre 0 y 10:
Rango de valores
<xsd:simpleType name="nota">
<xsd:restriction base="decimal">
<xsd:minInclusive value="0" fixed="true" />
<xsd:maxInclusive value="10" fixed="true" />
</xsd:restriction>
</xsd:simpleType>
El tipo nota que hemos definido está basado en el tipo intrínseco decimal y, en su definición,
se especifican dos facetas (su valor mínimo y su valor máximo). El uso de fixed evita que
alguien pueda modificar las facetas especificadas en la definición del tipo (por ejemplo, al
declarar un nuevo tipo basado en nota).
Tipo enumerado
<xsd:simpleType name="sexo">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="hombre"/>
<xsd:enumeration value="mujer"/>
<xsd:enumeration value="???"/>
</xsd:restriction>
</xsd:simpleType>
O incluso especificar un patrón al que han de atenerse los valores válidos de un tipo (utilizando
expresiones regulares):
<xsd:simpleType name="SKU">
<xsd:restriction base="xsd:string">
<xsd:pattern value="\d{3}-[A-Z]{2}"/>
</xsd:restriction>
</xsd:simpleType>
Además de poder especificar tipos enumerados y expresiones regulares, las facetas incluidas
en el estándar XSD permiten especificar la longitud de una cadena o de una lista ( length,
minLength y maxLength), si se permite la presencia de espacios en blanco (whiteSpace), el
intervalo de valores permitido (minInclusive, minExclusive, maxInclusive,
maxExclusive) y el número de dígitos de un valor decimal ( totalDigits y
fractionDigits).
Estructuras de datos
Los tipos complejos como el utilizado en el ejemplo del almacén son similares a los tipos
enumerados y, en concreto, suelen utilizarse para representar tablas. Si le asignamos un
nombre al tipo especificado, lo que estamos haciendo es definir un tipo abstracto que se podrá
utilizar en la definición de otros elementos, como en:
<xsd:schema
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:element name="purchaseOrder"
type="PurchaseOrderType"/>
<xsd:complexType name="PurchaseOrderType">
<xsd:sequence>
<xsd:element name="shipTo" type="Address"/>
<xsd:element name="billTo" type="Address"/>
<xsd:element ref="comment" minOccurs="0"/>
<xsd:element name="items" type="Items"/>
</xsd:sequence>
<xsd:attribute name="orderDate" type="xsd:date"/>
</xsd:complexType>
<xsd:complexType name="Address">
<xsd:sequence>
<xsd:element name="name" type="xsd:string"/>
<xsd:element name="street" type="xsd:string"/>
<xsd:element name="city" type="xsd:string"/>
<xsd:element name="zip" type="xsd:decimal"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="Items">
<xsd:sequence>
<xsd:element name="item"
minOccurs="0" maxOccurs="unbounded">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="product" type="xsd:string"/>
<xsd:element name="quantity" type="xsd:integer" />
<xsd:element name="price" type="xsd:decimal"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>
Cuando nos interesa permitir que en dentro de un elemento aparezcan elementos alternativos
en vez de una secuencia de ellos, podemos utilizar un bloque choice:
<xsd:complexType name="PurchaseOrderType">
<xsd:sequence>
<xsd:choice>
<xsd:group ref="shipAndBill"/>
<xsd:element name="singleAddress" type="Address"/>
</xsd:choice>
<xsd:element ref="comment" minOccurs="0"/>
<xsd:element name="items" type="Items"/>
</xsd:sequence>
<xsd:attribute name="orderDate" type="xsd:date"/>
</xsd:complexType>
<xsd:group name="shipAndBill">
<xsd:sequence>
<xsd:element name="shipTo" type="Address"/>
<xsd:element name="billTo" type="Address"/>
</xsd:sequence>
</xsd:group>
Aparte de definir tipos estructurados, XSD nos permite definir listas como si fuesen tipos de
datos simples. Por ejemplo, en el documento XML:
<xsd:simpleType name="lista">
<xsd:list itemType="xsd:integer"/>
</xsd:simpleType>
Además, los esquemas XML nos permiten definir claves, tanto primarias como externas:
<xsd:schema targetNamespace="http://elvex.ugr.es/informe"
xmlns="http://elvex.ugr.es/informe"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
attributeFormDefault="unqualified"
elementFormDefault="qualified">
<xsd:annotation>
<xsd:documentation xml:lang="es">
Informe de proveedores y piezas
</xsd:documentation>
</xsd:annotation>
<xsd:element name="informe">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="proveedores"
type="Proveedores">
<xsd:keyref name="refPieza" refer="keyPieza">
<xsd:selector xpath="proveedor/pieza" />
<xsd:field xpath="@sku" />
</xsd:keyref>
</xsd:element>
</xsd:sequence>
<xsd:attribute name="fecha" type="xsd:date" />
</xsd:complexType>
<xsd:unique name="uniqProveedor">
<xsd:selector xpath="proveedores/proveedor" />
<xsd:field xpath="@id" />
</xsd:unique>
<xsd:key name="keyPieza">
<xsd:selector xpath="piezas/pieza" />
<xsd:field xpath="@sku" />
</xsd:key>
</xsd:element>
<xsd:complexType name="Proveedores">
<xsd:sequence>
<xsd:element name="proveedor"
maxOccurs="unbounded">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="suministro"
maxOccurs="unbounded">
<xsd:complexType>
<xsd:complexContent>
<xsd:restriction
base="xsd:anyType">
<xsd:attribute name="sku"
type="SKU" />
<xsd:attribute name="cantidad"
type="xsd:positiveInteger" />
</xsd:restriction>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
</xsd:sequence>
<xsd:attribute name="id"
type="xsd:positiveInteger" />
</xsd:complexType>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="Piezas">
<xsd:sequence>
<xsd:element name="pieza" maxOccurs="unbounded">
<xsd:complexType>
<xsd:simpleContent>
<xsd:extension base="xsd:string">
<xsd:attribute name="sku" type="SKU" />
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
<xsd:simpleType name="SKU">
<xsd:restriction base="xsd:string">
<xsd:pattern value="\d{3}-[A-Z]{2}" />
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>
<informe xmlns="http://elvex.ugr.es/informe"
fecha="2002-12-31">
<proveedores>
<proveedor id="95819">
<suministro sku="872-AA" cantidad="1" />
<suministro sku="926-AA" cantidad="1" />
<suministro sku="833-AA" cantidad="1" />
<suministro sku="455-BX" cantidad="1" />
</proveedor>
<proveedor id="63143">
<suministro sku="455-BX" cantidad="4" />
</proveedor>
</proveedores>
<piezas>
<pieza sku="872-AA">Monitor</pieza>
<pieza sku="926-AA">Impresora</pieza>
<pieza sku="833-AA">Escáner</pieza>
<pieza sku="455-BX">CD-R</pieza>
</piezas>
</informe>
Información adicional
http://www.w3c.org/TR/xmlschema-0/
XML en Visual Studio .NET
Vamos a ilustrar cómo crear un esquema XML para un problema en el que se gestionan
pedidos.
En el menú Archivo, elija Nuevo y, a continuación, haga clic en Proyecto para mostrar el
cuadro de diálogo Nuevo proyecto. Seleccione Proyectos de Visual
Antes de crear la tabla relacional, primero creará definiciones de tipos simple y complejo que
utilizará para dar formato a elementos específicos del esquema de pedido. Los nuevos tipos se
crean utilizando tipos de datos XML existentes, como string e integer.
En primer lugar definirá un tipo simple, que se denominará CodigoProvincia. Este tipo
simple se utilizará para limitar el tamaño de una cadena a dos caracteres.
Haga clic en la ficha XML en la parte inferior izquierda del Diseñador XML, para
ver el código XML que se ha agregado:
<xs:simpleType name="CodigoProvincia">
<xs:restriction base="xs:string">
<xs:length value="2" />
</xs:restriction>
</xs:simpleType>
Para ver el código XML que se ha agregado al archivo .xsd, haga clic en la
ficha XML en la parte inferior del diseñador. Verá el siguiente código XML:
<xs:complexType name="TipoDireccion">
<xs:sequence>
<xs:element name="Nombre" type="xs:string" />
<xs:element name="Calle" type="xs:string" />
<xs:element name="Ciudad" type="xs:string" />
<xs:element name="Provincia"
type="CodigoProvincia" />
<xs:element name="CodPostal" type="xs:integer"/>
</xs:sequence>
</xs:complexType>
A continuación, es posible agregar elementos adicionales bajo complexType para definir los
campos (o columnas) de la relación. Si define uno de estos nuevos elementos como un nuevo
complexType sin nombre, está creando una relación anidada dentro de la relación primaria
con sus propias columnas únicas.
Se trata de crear una taba relacional llamada Pedidos, agregar un elemento Items a esa
tabla, especificando que Items sea, a su vez, de tipo complexType sin nombre, lo que
permite la repetición de objetos Item para un sólo registro de pedido.
Como se está definiendo una nueva tabla relacional hace que aparezca un nuevo elemento en
la superficie de diseño. En la relación de nuevos Items , al agregar el elemento Item y
establecer su tipo en complexType sin nombre, se crea otra tabla relacional, que también
aparece en el superficie de diseño.
<xs:element name="Pedidos">
<xs:complexType>
<xs:sequence>
<xs:element name="EnviarA"
type="TipoDireccion" />
<xs:element name="FacturarA"
type="TipoDireccion" />
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:simpleType name="CodigoProvincia">
<xs:restriction base="xs:string">
<xs:length value="2" />
</xs:restriction>
</xs:simpleType>
<xs:complexType name="TipoDireccion">
<xs:sequence>
<xs:element name="Nombre" type="xs:string" />
<xs:element name="Calle" type="xs:string" />
<xs:element name="Ciudad" type="xs:string" />
<xs:element name="Provincia"
type="CodigoProvincia" />
<xs:element name="CodPostal"
type="xs:integer" />
</xs:sequence>
</xs:complexType>
<xs:element name="Pedidos">
<xs:complexType>
<xs:sequence>
<xs:element name="EnviarA"
type="TipoDireccion" />
<xs:element name="FacturarA"
type="TipoDireccion" />
<xs:element name="Items">
<xs:complexType>
<xs:sequence>
<xs:element name="Item" minOccurs="1"
maxOccurs="unbounded">
<xs:complexType>
<xs:sequence>
<xs:element name="Cantidad"
type="xs:integer" />
<xs:element name="Precio"
type="xs:decimal" />
<xs:element name="IDProducto"
type="xs:integer" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
Crear ficheros de datos XML, asociar esquemas y validar datos.
Los ficheros XML son ficheros de texto y como tales pueden crearse con cualquier editor que
guarde los documentos en formato texto. Nosotros usaremos, por comodidad, Visual Studio
.NET para crear un fichero de datos XML.
En primer lugar asociaremos a este fichero XML el esquema que creamos anteriormente. En la
ventana de Propiedades, seleccionar DOCUMENT, y en la propiedad targetSchema
seleccionar el esquema de la lista desplegable:
Antes de empezar con el esquema debemos tener en cuenta algunas consideraciones que nos
facilitarán la tarea de diseño:
El precio de un libro debe ser de tipo real (con decimales...) y el valor mínimo
debe ser 0.
Queremos que la etiqueta de cada género tenga una longitud mínima de 1
carácter.
El número de páginas debe ser un valor entero y positivo.
Se trata de definir en primer lugar los tipos simples y a continuación los tipos complejos que
hagan uso de los tipos simples ya definidos. Dejaremos los géneros para el final.
Cambie element1 a Biblioteca para asignar un nombre al elemento. Puede dejar el tipo de
datos como queda por defecto: (Biblioteca).
Recordemos que cada libro tiene, al menos, un género asociado y que queremos restringir el
número máximo de géneros a 5. La idea es que si un libro tiene, por ejemplo, dos géneros, su
código XMl será así:
...
<Biblioteca>
<Libro>
<Autor>Arturo Pérez Reverte</Autor>
<Titulo>El capitán Alatriste</Titulo>
...
<Generos>
<Genero>Novela</Genero>
<Genero>Historica</Genero>
</Generos>
</Libro>
...
</Biblioteca>
Cree el tipo simple TipoGenero que está basado en el tipo string
restringiendo la longitud mínima a 1.
<xs:simpleType name="TipoPrecio">
<xs:restriction base="xs:float">
<xs:minInclusive value="0.0" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="TipoGenero">
<xs:restriction base="xs:string">
<xs:minLength value="1" />
</xs:restriction>
</xs:simpleType>
<xs:complexType name="TipoLibro">
<xs:sequence>
<xs:element name="Titulo" type="xs:string" />
<xs:element name="Autor" type="xs:string" />
<xs:element name="NumPags"
type="xs:positiveInteger" />
<xs:element name="Precio" type="TipoPrecio"/>
<xs:element name="Generos">
<xs:complexType>
<xs:sequence>
<xs:element name="Genero"
type="TipoGenero"
maxOccurs="5" minOccurs="1" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="Disponible" type="xs:boolean" />
</xs:complexType>
<xs:element name="Biblioteca">
<xs:complexType>
<xs:sequence>
<xs:element name="Libro" type="TipoLibro"
minOccurs="1" maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
Añadir al proyecto un fichero XML (menú Proyecto, Agregar nuevo elemento, Archivo
XML) llamado LibrosDeCasa.xml.
Escribir el contenido del fichero y validar los datos contra el esquema. Utilizar estos datos para
el ejemplo, añadiendo más si lo considera oportuno.
<Libro Disponible="false">
<Titulo>El Rabino</Titulo>
<Autor>Noah Gordon</Autor>
<NumPags>650</NumPags>
<Precio>100.0</Precio>
<Generos>
<Genero>Religioso</Genero>
<Genero>Novela</Genero>
<Genero>Moderna</Genero>
</Generos>
</Libro>
<Libro Disponible="true">
<Titulo>El Médico</Titulo>
<Autor>Noah Gordon</Autor>
<NumPags>890</NumPags>
<Precio>200.0</Precio>
<Generos>
<Genero>Novela</Genero>
<Genero>Histórica</Genero>
</Generos>
</Libro>
<Libro Disponible="true">
<Titulo>La Sangre de Dios</Titulo>
<Autor>Nicholas Wilcox</Autor>
<NumPags>300</NumPags>
<Precio>300.0</Precio>
<Generos>
<Genero>Novela</Genero>
<Genero>Ciencia Ficción</Genero>
</Generos>
</Libro>
<Libro Disponible="true">
<Titulo>El Capitán Alatristre</Titulo>
<Autor>Arturo Pérez Reverte</Autor>
<NumPags>450</NumPags>
<Precio>100.0</Precio>
<Generos>
<Genero>Histórica</Genero>
<Genero>Novela</Genero>
</Generos>
</Libro>
</Biblioteca>
Presentación y transformación de
documentos XML
Ventajas:
Desventajas:
Para usar una hoja de estilo CSS para presentar el contenido de un documento XML hay que
añadir la siguiente línea en el prólogo:
Un ejemplo sencillo
/* Fichero: MuySencillo.css */
Titulo
{
display:block;
margin-top:12pt;
font-size:15pt
}
Autor
{
display:block;
color:Aqua;
font-style:italic
}
Precio
{
font-weight:bold
}
NumPags
{
font-size: 10pt;
left: 30pt;
color: maroon;
font-style: italic;
font-family: Arial;
position: static;
top: 10pt;
}
Generos
{
display: none;
}
<Biblioteca xmlns="http://tempuri.org/EsquemaBiblioteca.xsd">
<Libro Disponible="false">
<Titulo>El Rabino</Titulo>
<Autor>Noah Gordon</Autor>
<NumPags>650</NumPags>
......
Visual Studio .NET proporciona facilidades para asociar a un proyecto hojas de estilo CSS y
para construirlas empleando un asistente. Su uso es muy sencillo y pueden construirse hojas
de estilo complejas sin necesidad de conocer nada acerca de CSS.
En primer lugar veremos cómo crear una hoja de estilo con Visual Studio .NET.
Se trata de especificar el formato de cada elemento del documento XML especificando una
regla para cada elemento.
Como no hay ningún elemento llamado body lo borramos y escribimos, por ejemplo, Titulo.
Observar como se modifica el explorador de elementos en la parte izquierda del diseñador
CSS:
Ahora podremos especificar la transformación asociada al elemento Titulo. El formato
genérico es el que se muestra en el ejemplo:
estilos que nos permite definir fácilmente el estilo del elemento seleccionado. El usuario
selecciona categorías de formato (fuente, color, etc) y especifica valores y Visual Studio .NET
escribe en el fichero CSS el código CSS correspondiente.
Dominar este asistente es cuestión de experimentar con él... Sin embargo debe saber dos
cosas importantes:
Solo aparecen con formato los elementos que se han especificado y configurado.
Aparecerán todos los elementos y aquellos que no hatan sido configurado se
mostrarán de cualquier manera.
Si no quiere que aparezca un elemento debe especifcarlo en el formato. Por ejemplo,
si solo quisiera mostrar los autores de los libros se añadiría el elemento Autor y se
definiría el formato siguiente: Diseño, Mostrar y No mostrar
Completar la definición de formatos en MiHojaCSS.css para que quede así:
/* Fichero: MiHojaCSS.css */
Titulo
{
display: block;
font-weight: normal;
font-size: 15pt;
text-transform: capitalize;
color: red;
}
Generos
{
display: none;
}
Autor
{
display: none;
}
Precio
{
display: none;
}
NumPags
{
display: none;
}
XSL es un lenguaje que nos permite definir una representación o formato pra un documento
XML. Un mismo documento XML puede tener varias hoas de estilo XSL que lo muestren en
diferentes formatos (HTML, PDF, RTF, PostScript, etc.) Básicamente, XSL es un lenguaje que
define la transformación entre un documento XML de entrada y otro documento XML de salida.
La aplicación de una hoja de estilo XSL a un documento XML puede ocurrir tanto en el origen
(por ejemplo, un servlet que convierta de XML a HTML para que sea mostrado en el navegador
de un ordenador conectado a un servidor web) o en el mismo navegador (por ejemplo,
Internet Explorer 6).
XSLT: Lenguaje para transformar documentos XML en otro formato (otro XML, HTML,
DHTML, texto plano, PDF, RTF, Word, etc.)
XSL-FO (XSL Formatting Objects): Especificación que trata cómo deben ser los
objetos de formato para convertir XML a formatos binarios (PDF, Word, imágenes, etc.).
Todavía no ha alcanzado el estado de "recomendado por la W3C".
XPath: Lenguaje de consulta genérico para identificar (y seleccionar) elementos de
un documento XML.
Ventajas y desventajas
Ventajas:
La salida no tiene por qué ser HTML para visualización en un navegador, sino que
puede estar en muchos formatos.
Permite manipular de muy diversas maneras un documento XML: reordenar
elementos, filtrar, añadir, borrar, etc.
Permite acceder a todo el documento XML, no sólo al contenido de los elementos.
XSLT es un lenguaje XML, por lo que no hay que aprender nada especial acerca de
su sintaxis.
Desventajas:
Formas de uso
</xsl:stylesheet>
Para usar un fichero XSLT desde un documento XML hay que añadir la siguiente línea en el
prólogo:
Por ejemplo, modificaremos el fichero LibrosDeCasa.xml para que quede como sigue:
<Biblioteca>
<Libro Disponible="false">
<Titulo>El Rabino</Titulo>
<Autor>Noah Gordon</Autor>
<NumPags>650</NumPags>
......
La idea es muy sencilla y todo está basado en considerar que los elementos que
forman el documento XML mantienen una relación jerárquica: son nodos del árbol que
determina la relación entre los elementos del documento XML.
Una hoja de estilo XSL consta de una serie de reglas que determinan cómo va a
ocurrir la transformación. Cada regla se compone de:
o patrón (pattern).
o plantilla (template) o acción.
Cada regla afecta a uno o varios elementos del documento XML.
Sintácticamente, las reglas tienen tres partes:
o La marca de apertura que contiene un atributo match que describe a qué
partes del documento se aplica la regla (qué nodos están afectados). La sintaxis del
patrón (valor del atributo match) debe seguir las especificaciones del lenguaje
XPath. En definitiva: un patrón es una expresión XPath.
o La parte central describe qué debe hacerse cuando se produce una
coincidencia.
o La marca de cierre.
Así, cada elemento template se asocia con un fragmento del documento XML (que
puede ser un elemento o un conjunto de elementos) y se transforma en otro fragmento
de XML o HTML, de acuerdo a lo que se especifique en su interior.
Además de xsl:template, los elementos del espacio de nombres xsl (¿instrucciones XSL?)
que nos ayudan a escribir transformaciones son:
xsl:apply-templates hace que se apliquen las reglas que siguen a todos los
nodos seleccionados.
<xsl:apply-templates />
<xsl:apply-templates select="PATRON">
.....
<xsl:apply-templates />
xsl:value-of extrae un valor concreto (literal) del documento.
xsl:for-each aplica una acción repetidamente para cada nodo de un conjunto. En
definitiva, se usa para iterar sobre una serie de elementos.
<xsl:for-each select="PLANTILLA">
.....
<xsl:for-each />
<xsl:choose>
<xsl:when test="EXPRESIÓN 1"> ... </xsl:when />
<xsl:when test="EXPRESIÓN 2"> ... </xsl:when />
.....
<xsl:otherwise> ... </xsl:otherwise />
<xsl:choose />
<xsl:sort select="/Biblioteca/Libro/NumPags"
order="ascending" />
En este ejemplo hemos hecho uso de un patrón algo complejo que responde a una
expresión de XPath que puede interpretarse así: De la raiz del documento XML (todo el
documento) seleccionar los elementos Biblioteca
y de éstos, los que tengan como etiqueta Libro. Finalmente, de todos éstos
seleccionaremos únicamente los elementos NumPags.
XPath
Los patrones pueden ser muy complejos y, como hemos indicado, se especifican en un
lenguaje llamado XPath. Es un lenguaje de consulta usado para identificar y seleccionar nodos
(elementos) de un documento XML. Se caracteria por:
Los operados empleados habitualmente para formar los patrones se describen en la siguiente
tabla:
Operador Descripción
Selección de hijo. Selecciona únicamente a los
/ descendientes directos. Al principio del patrón el contexto
es la raíz del documento.
Selección de descendientes. Selecciona todos los
//
descendientes.
. Selección del elemento actual (el contexto).
* Todos (en el sentido habitual de este operador)
@ Prefijo que se antepone al nombre de un atributo.
[] Filtro sobre el conjunto de nodos seleccionado.
Algunos ejemplos:
./AUTOR Selecciona todos los elementos AUTOR dentro del contexto actual (todos los hijos del
nodo actual que tengan como etiqueta AUTOR).
/LIBROS Selecciona los elementos (posiblemente uno solo) con etiqueta LIBROS que cuelgan
directamente de la raiz.
//AUTOR Selecciona todos los elementos AUTOR en cualquier parte del documento.
Ejemplo 1
Se trata de mostrar una lista con todos los libros. Se incluye un encabezado y solo se muestra
el título de cada libro.
<Biblioteca>
......
Ejemplo 2
Vamos a generar como salida código HTML. La información a mostrar es el título del libro y el
autor. Usaremos el siguiente fichero XSLT:
<xsl:template match="Autor">
<FONT COLOR="#FF0000">
<xsl:value-of select="." />
</FONT>
</xsl:template>
</xsl:stylesheet>
El fichero XML quedará ahora:
<?xml-stylesheet type="text/xsl"
href="XSLTSencilloHTML.xslt"?>
<Biblioteca>
......
Ejemplo 3
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<HTML>
<HEAD><TITLE>Informe por géneros</TITLE></HEAD>
<BODY BGCOLOR="#D2D2D2">
<H2>Informe detallando los géneros</H2>
<xsl:for-each select="/Biblioteca/Libro">
<HR />
<B>Titulo: </B>
<I><xsl:value-of select="Titulo" /></I><BR />
<B>Autor : </B>
<I><xsl:value-of select="Autor" /></I><BR/>
<B>Temas: </B>
<xsl:for-each select="Generos/Genero">
<xsl:value-of select="." />,
</xsl:for-each>
<HR />
</xsl:for-each>
</BODY>
</HTML>
</xsl:template>
</xsl:stylesheet>
Ejemplo 4
En este ejemplo practicamos con la construcción de selección condicional xsl:if para mostrar
una lista de libros voluminosos.
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<HTML>
<xsl:apply-templates select="Biblioteca" />
</HTML>
</xsl:template>
<xsl:template match="Biblioteca">
<HEAD><TITLE>Libros gordos</TITLE></HEAD>
<BODY BGCOLOR="#FAFAFA">
<H2>Mis libros voluminosos</H2>
<OL>
<xsl:apply-templates select="Libro" />
</OL>
</BODY>
</xsl:template>
<xsl:template match="Libro">
</xsl:template>
</xsl:stylesheet>
Si quisiéramos hacer más versátil esta hoja de estilo y quisiéramos que el número de páginas
mínimo fuera fácilmente modificable, de manera que solo hubiera que hacer un cambio para
generar una nueva lista podríamos usar una variable o, en la terminología adecuada, un
elemento xsl:parameter. Los cambios a realizar serían los siguientes:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
......
<P>Los libros de la biblioteca que tienen más de
<xsl:value-of select="$NumPagsVol"/>
páginas son: </P>
......
......
<xsl:if test="NumPags > $NumPagsVol">
......
Ejemplo 5
En este ejemplo practicamos con casi todos los elementos para construir una tabla que
recopila toda la información del documento XML.
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<HTML>
<HEAD><TITLE>Tabla ordenada de libros</TITLE></HEAD>
</xsl:template>
<xsl:template match="Biblioteca">
<BODY BGCOLOR="#FAFAFA">
<xsl:for-each select="Libro">
<cuerpo-tabla>
<TR>
<TD><xsl:value-of select="Titulo"/></TD>
<TD><xsl:value-of select="Autor"/></TD>
<TD><xsl:value-of select="NumPags"/></TD>
<TD><xsl:value-of select="Precio"/></TD>
<TD ALIGN="CENTER">
<xsl:if test="@Disponible='true'">
<FONT COLOR="#00FF00"><B>SI</B></FONT>
</xsl:if>
<xsl:if test="@Disponible='false'">
<FONT COLOR="#FF0000"><B>NO</B></FONT>
</xsl:if>
</TD>
<TD>
<xsl:for-each select="Generos/Genero">
<xsl:value-of select="." /> -
</xsl:for-each>
</TD>
</TR>
</cuerpo-tabla>
</xsl:for-each>
</TABLE>
</BODY>
</xsl:template>
</xsl:stylesheet>
XML en la plataforma .NET
En la figura siguiente se muestra cómo se estructura la memoria cuando se leen los datos de
la biblioteca (XML) en la estructura DOM.
Dentro de la estructura de los documentos XML, cada elipse de esta ilustración representa un
nodo, que se denomina objeto XmlNode. El objeto XmlNode es el objeto básico del árbol DOM.
La clase XmlDocument, que extiende la clase XmlNode, admite métodos para realizar
operaciones en el documento en conjunto, por ejemplo, cargarlo en la memoria o guardar el
código XML en un archivo. Además, la clase XmlDocument proporciona un medio para ver y
manipular los nodos de todo el documento XML.
Tipos de nodos
A medida que el contenido XML se lee en el DOM las partes se traducen en nodos que
mantienen metadatos adicionales acerca de sí mismos, como su tipo y valores. Esto es, cuando
se leen varios datos, se asigna a cada nodo un tipo. Esto es importante ya que no todos los
nodos son del mismo tipo y e tipo determina las características y funcionalidad del nodo (qué
acciones pueden realizarse y qué propiedades pueden establecerse o recuperarse).
En la tabla siguiente se muestran algunos tipos de nodo, el objeto asociado a dicho tipo y una
breve dscripción.
Los objetos DOM tienen un conjunto de métodos y propiedades, así como características
básicas y bien definidas. Algunas de estas características son:
La forma de controlar los atributos es una característica de DOM. Los atributos no son
elementos secundarios de un elemento sino propiedades del elemento y están formados por un
par nombre-valor.
Es importante hacer esta distinción, debido a los métodos utilizados para desplazarse por los
nodos relacionados, principales y secundarios del DOM. Por ejemplo, los métodos
PreviousSibling y NextSibling no se utilizan para desplazarse de un elemento a un
atributo, ni entre atributos. En su lugar, un atributo es una propiedad de un elemento y
pertenece a un elemento, tiene una propiedad OwnerElement y no una propiedad
parentNode, y tiene métodos de desplazamiento distintos.
Conclusiones
DOM resulta útil para leer datos XML en la memoria y cambiar su estructura, agregar
o quitar nodos, o modificar los datos mantenidos en un nodo como en el texto
contenido en un elemento. No obstante, hay otras clases disponibles que son más rápidas
que DOM en otros escenarios:
La plataforma .NET proporciona soporte para XML en el espacio de nombres System.Xml y los
subespacios:
System.Xml.Xsl.
System.Xml.XPath.
System.Xml.Schema.
System.Xml.Serialization.
System.Xml proporciona clases para crear, modificar y navegar en documentos XML. También
facilita el poder leer y escribir XML usando DOM proporcionando clases como
XPathNavigator , XmlDataDocument y XmlDocument. Además, permite manipular
elementos XML con las clases XmlElement, XmlAttribute, XmlComment, etc.
Miembros de XmlDocument
Constructores
XmlDocument Inicializa una nueva instancia de la clase XmlDocument.
Propiedades
públicas
Obtiene un objeto XmlAttributeCollection que contiene
Attributes
los atributos de este nodo.
ChildNodes Obtiene todos los nodos secundarios del nodo.
DocumentElement Obtiene el XmlElement raíz del documento.
FirstChild Obtiene el primer nodo secundario del nodo.
Obtiene un valor que indica si este nodo tiene nodos
HasChildNodes
secundarios.
LatChild Obtiene el último nodo secundario del nodo.
LocalName / Name Obtiene el nombre local/completo del nodo.
NextSibling Obtiene el nodo inmediatamente siguiente a éste.
NodeType Obtiene el tipo del nodo actual.
OwnerDocument Obtiene el XmlDocument al que pertenece el nodo actual.
Obtiene el nodo primario de este nodo (para nodos que
ParentNode
pueden tener nodos primarios).
PreviousSibling Obtiene el nodo inmediatamente anterior a éste.
Value Obtiene o establece el valor del nodo.
Métodos públicos
Agrega el nodo especificado al final de la lista de nodos
AppendChild
secundarios de este nodo.
CreateAttribute Crea un XmlAttribute con el nombre especificado.
El siguiente código muestra algunas estadísticas calculadas a partir de los datos XML:
using System;
using System.IO;
using System.Xml;
class BibliotecaApp
{
XmlNodeList ListaLibros =
doc.SelectNodes ("/Biblioteca/Libro");
int nl = ListaLibros.Count;
Console.WriteLine ("Numero total de libros = {0}", nl);
XmlNodeList ListaPrecios =
doc.SelectNodes ("/Biblioteca/Libro/NumPags");
Console.ReadLine();
}
}
Cuando el nodo actual es un elemento, utilice el método HasAttribute para ver si hay algún
atributo asociado a dicho elemento. Una vez que se sabe que un elemento tiene atributos,
existen múltiples métodos de acceso a atributos. Para recuperar un único atributo de un
elemento, utilice los métodos GetAttribute y GetAttributeNode de XmlElement, u
obtenga todos los atributos en una colección. La obtención de la colección resulta útil si es
necesario recorrerla en iteración. Si desea obtener todos los atributos del elemento, utilice la
propiedad Attributes del elemento para recuperar todos los atributos en una colección.
Si desea obtener todos los atributos de un nodo de elemento en una colección, llame a la
propiedad XmlElement.Attributes. De este modo se obtiene la
XmlAttributeCollection que contiene todos los atributos de un elemento. Cada elemento
de la colección de atributos representa un nodo XmlAttribute. Para buscar el número de
atributos de un elemento, obtenga el XmlAttributeCollection y utilice la propiedad Count
para saber cuántos nodos XmlAttribute hay en la colección.
La información de una colección de atributos puede recuperarse por nombre (ver la línea de
código XmlAttribute atr =
using System;
using System.IO;
using System.Xml;
class BibliotecaApp
{
// De manera resumida:
// XmlAttributeCollection atrCol =
// doc.DocumentElement.Attributes;
if (Raiz.HasAttribute("ISBN"))
{
// Seleccionar nombre
XmlAttribute atr = atrCol["ISBN"];
// Seleccionar valor
String ValorISBN = atr.InnerXml;
Console.WriteLine();
Console.Write ("Valor del atributo ISBN: ");
Console.WriteLine(ValorISBN);
}
Console.ReadLine();
}
}
..........
// Colocarse en la raiz del documento.
XmlElement Raiz = doc.DocumentElement;
// Recuperar el atributo ISBN.
XmlAttribute atr = Raiz.GetAttributeNode("ISBN");
// Seleccionar valor
String ValorISBN = atr.InnerXml;
// Mostrar su valor
Console.Write ("Valor del atributo ISBN: ");
Console.WriteLine(ValorISBN);
// De otra manera:
String ValorISBNAlt = Raiz.GetAttribute("ISBN");
Console.Write ("Valor del atributo ISBN (alt): ");
Console.WriteLine(ValorISBNAlt);
// Calcular y mostrar nodo propietario
XmlNode node= atr.OwnerElement;
String cad = node.Name;
Console.WriteLine ("Pertenece a un nodo: '{0}'", cad);
Console.ReadLine();
}
}
Las clases XmlReader y XmlWriter
XmlReader es una clase base abstracta que proporciona acceso de sólo avance y de sólo
lectura sin almacenamiento en caché. Esto significa, por ejemplo, que no hay funciones para
editar los valores de un atributo o contenido de un elemento, ni la posibilidad de agregar y
quitar nodos. La lectura se realiza por el método de primero en profundidad (como se lee
textualmente XML).
El nodo actual hace referencia al nodo en el que está situado el lector. Para avanzar el lector,
utilice cualquiera de los métodos de lectura. Las propiedades reflejan el valor del nodo actual.
Leer contenido XML cuando éste está disponible en su totalidad, por ejemplo,
un archivo de texto XML.
Buscar la dimensión de la pila de elementos XML.
Determinar si un elemento tiene contenido o está vacío.
Leer y explorar atributos.
Omitir elementos y su contenido.
Xmlwriter es una clase base abstracta que proporciona acceso de sólo avance y de sólo
escritura sin almacenamiento en caché para escribir código XML.
En la lista siguiente se muestra el propósito de los métodos y propiedades que incluye la clase
XmlWriter:
La transformación se aplica con el método Transform que transforma los datos XML utilizando
la hoja de estilos XSLT que se ha cargado.
Ejemplo 1
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/Biblioteca">
<ResumenBiblioteca>
<xsl:apply-templates />
</ResumenBiblioteca>
</xsl:template>
<xsl:template match="Libro">
<item>
<xsl:value-of select="Titulo" />
</item>
</xsl:template>
</xsl:stylesheet>
using System;
using System.IO;
using System.Xml;
using System.Xml.Xsl;
class BibliotecaApp
{
static void Main(string[] args)
{
// Crear objeto XSLT y leer fichero XSLT.
Console.ReadLine();
}
}
xslt.Transform("../../LibrosDeCasa.xml",
"../../Resumen.xml", null);
.........
using System;
using System.IO;
using System.Xml;
using System.Xml.Xsl;
class BibliotecaApp
{
static void Main(string[] args)
{
// Crear objeto XSLT y leer fichero XSLT.
XmlTextWriter writer =
new XmlTextWriter (Console.Out);
writer.Formatting = Formatting.Indented;
writer.Indentation = 4;
writer.WriteStartDocument(true);
Console.ReadLine();
}
}
que produce como resultado:
Para que el resultado con formato se guarde en un fichero modificamos el programa para que
quede como sigue:
using System;
using System.IO;
using System.Xml;
using System.Xml.Xsl;
class BibliotecaApp
{
static void Main(string[] args)
{
// Crear objeto XSLT y leer fichero XSLT.
XslTransform xslt = new XslTransform();
xslt.Load("../../TransfSimple.xslt");
// Crear objeto XML y leer fichero XML.
XmlDocument doc = new XmlDocument();
doc.Load("../../LibrosDeCasa.xml");
XmlTextWriter rdo = new
XmlTextWriter("../../Resumen2.xml", null);
rdo.Formatting = Formatting.Indented;
rdo.WriteStartDocument(true);
xslt.Transform(doc, null, rdo, null);
Console.ReadLine();
}
}
y el resultado es:
La modificación propuesta ahora:
......
XmlTextWriter rdo =
new XmlTextWriter("../../Resumen3.xml",
System.Text.Encoding.UTF8);
......
hace que se modifique el prólogo del fichero XML incorporando la codificación UTF-8:
El espacio de nombres System.Xml.XPath
El espacio de nombres System.Xml.XPath contiene el motor de evaluación y el analizador
XPath. Es compatible con la Recomendación XML Path Language (XPath) Version 1.0 del
Consorcio W3C (www.w3.org/TR/xpath).
El siguiente ejemplo usa el modelo de cursor para calcular el precio medio de los libros. Utilice
el XPathNodeIterator para establecer una iteración en un conjunto de nodos.
using System;
using System.IO;
using System.Xml;
using System.Xml.XPath;
class BibliotecaApp
{
static void Main(string[] args)
{
// Abre para lectura el fichero XML
XPathDocument doc =
new XPathDocument ("../../LibrosDeCasa.xml");
// Crea un objeto XPathNavigator (nav) para
// desplazarse por doc
XPathNavigator nav = doc.CreateNavigator();
int nl = it.Count;
Console.WriteLine ("Numero total de libros= {0}",nl);
Console.ReadLine();
}
}
El método Evaluate evalúa la cadena que representa a una expresión XPath y devuelve el
resultado de tipo (número, valor booleano, cadena o conjunto de nodos).
using System;
using System.IO;
using System.Xml;
using System.Xml.XPath;
class BibliotecaApp
{
static void Main(string[] args)
{
// Abre para lectura el fichero XML
XPathDocument doc =
new XPathDocument ("../../LibrosDeCasa.xml");
int nl = it.Count;
Console.WriteLine ("Numero total de libros= {0}",nl);
// Evalua la expresion
double SumPrecio2 = (double) nav.Evaluate(expr);
Console.ReadLine();
}
}
using System;
using System.IO;
using System.Xml;
using System.Xml.XPath;
class BibliotecaApp2
{
static void Main(string[] args)
{
Console.WriteLine ("Lista de libros\n");
// Lee en doc un fichero XML
XmlDocument doc = new XmlDocument ();
doc.Load("../../MisLibros.xml");
int nl= 1;
int sum = 0;
Console.ReadLine();
}
}
"Serialización" de documentos XML
Introducción
La clase fundamental para esta tarea es XmlSerializer. Esta clase se emplea para serializar
y deserializar objetos en y desde documentos XML. XmlSerializer permite controlar el
modo en que se codifican los objetos en XML.
using System;
using System.IO;
using System.Xml.Serialization;
namespace XSDSample
{
public class Test
{
public String cad;
}
class SerialApp {
// Clase serializable
public class Test
{
public Test() {} // Igual que no ponerlo
public String cad;
}
// Clase NO serializable
public class Test2
{
public Test2(String cadena)
{
this.cad = cadena;
}
public String cad;
}
<?xml version="1.0"?>
<Cliente
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
/>
<?xml version="1.0"?>
<Cliente
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Nombre>Pepe</Nombre>
<Direccion>Casa de Pepe</Direccion>
<Telefono>123456789</Telefono>
</Cliente>
Ejemplo
using System;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
namespace SerDeserSample
{
public class Cliente
{
public string Nombre;
public string Direccion;
public string Telefono;
class SerDeserSampleApp {
static void Main(string[] args)
{
Cliente c = new Cliente();
c.Nombre = "Juan";
c.Direccion = "Casa de Juan S/N";
c.Telefono = "333444555";
// Serializar
XmlSerializer ser = new
XmlSerializer (typeof(Cliente));
FileStream OStream = new FileStream ("Juan.xml",
FileMode.OpenOrCreate);
// Deserializar
Cliente c2;
if(ser2.CanDeserialize(reader)) {
c2 = (Cliente) ser2.Deserialize (reader);
Console.WriteLine ("Despues de recuperar");
Console.WriteLine (c2);
}
else
Console.WriteLine("No puedo deserializar");
IStream.Close();
Console.ReadLine();
} // Main
} // class SerDeserSampleApp
} // manespace SerDeserSample
El fichero Juan.xml contiene:
<?xml version="1.0"?>
<Cliente
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Nombre>Juan</Nombre>
<Direccion>Casa de Juan S/N</Direccion>
<Telefono>333444555</Telefono>
</Cliente>
Personalizar la serialización
Pueden emplearse atributos en las clases y sus miembros que modifican el XML resultante de
la serialización.
using System;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
namespace SerDeserSample
{
[XmlRoot("cliente")]
public class Cliente
{
public string Nombre;
public string Direccion;
public string Telefono;
Console.WriteLine (c);
// Serializar
XmlSerializer ser = new
XmlSerializer (typeof(Cliente));
FileStream OStream = new FileStream ("cli.xml",
FileMode.OpenOrCreate);
// Deserializar
BuenCliente bc;
if(ser2.CanDeserialize(reader)) {
bc = (BuenCliente) ser2.Deserialize (reader);
Console.WriteLine (bc);
}
else
Console.WriteLine("No puedo deserializar");
IStream.Close();
Console.ReadLine();
} // Main
} // class SerDeserSampleApp
} // manespace SerDeserSample
El fichero cli.xml contiene:
<?xml version="1.0"?>
<cliente
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Nombre>Pepe</Nombre>
<Direccion>Casa de Pepe</Direccion>
<Telefono>123456789</Telefono>
</cliente>
[XmlRoot("Cliente")]
public class BuenCliente {
public string Telefono;
......
}
o o bien homogeneizar las clases origen y destino:
[XmlRoot("cliente")]
public class BuenCliente {
public string Nombre;
......
}
[XmlRoot("cliente")]
public class BuenCliente {
public string Telefono;
......
}
[XmlRoot("cliente")]
public class Cliente {
public string Nombre;
public string Direccion;
public string Telefono;
......
}
[XmlRoot("cliente")]
public class BuenCliente {
[XmlElement("Telefono")]
public string Tfno;
public string Dir;
[XmlElement("Nombre")]
public string Nbre;
......
}
[XmlRoot("MiCliente", Namespace="http://rocco.ugr.es")]
public class Cliente
{
public string Nombre;
public string Direccion;
public string Telefono;
}
[XmlRoot("MiCliente",
Namespace="http://rocco.ugr.es/Clis")]
public class Cliente {
public string Nombre;
[XmlElement(Namespace="http://elvex.ugr.es/Dirs")]
public TDireccion Direccion;
public string Telefono;
}
<?xml version="1.0"?>
<MiCliente
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://rocco.ugr.es/Clis">
<Nombre>Pepe</Nombre>
<Direccion
xmlns="http://elvex.ugr.es/Dirs">
<Calle>Calle Fina</Calle>
<Numero>4</Numero>
<Ciudad>Metrópolis</Ciudad>
<CP>12345</CP>
</Direccion>
<Telefono>123456789</Telefono>
</MiCliente>
Un elemento puede serializarse como un atributo XML.
[XmlRoot("MiCliente",
Namespace="http://rocco.ugr.es/Clis")]
public class Cliente {
public string Nombre;
[XmlElement(Namespace="http://elvex.ugr.es/Dirs")]
public TDireccion Direccion;
[XmlAttribute]
public string Telefono;
}
<?xml version="1.0"?>
<MiCliente
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
Telefono="123456789"
xmlns="http://rocco.ugr.es/Clis">
<Nombre>Pepe</Nombre>
<Direccion
CP="12345"
xmlns="http://elvex.ugr.es/Dirs">
<Calle>Calle Fina</Calle>
<Numero>4</Numero>
<Ciudad>Metrópolis</Ciudad>
</Direccion>
</MiCliente>
<Telefono>
<string>123456789</string>
<string>987654321</string>
......
</Telefono>
<TelefonosContacto>
<NumTelefono>123456789</NumTelefono>
<NumTelefono>987654321</NumTelefono>
......
</TelefonosContacto>
En este ejemplo describimos cómo se crea un documento XML complejo desde una aplicación
Windows. Se trata, concretamente, de crear el fichero de datos de la biblioteca, con el que
hemos trabajado frecuentemente.
La introducción de datos se realiza en una ventana diseñada con Visual Studio .NET cuyo
aspecto es el siguiente:
[XmlAttribute]
public bool Disponible;
[XmlArray("Generos")]
[XmlArrayItem("Genero")]
public string[] Generos;
}
[XmlRoot("Biblioteca",
Namespace="http://tempuri.org/EsquemaBiblioteca.xsd")]
public class Lista {
[XmlElement]
public CLibro[] Libro = new CLibro[10];
}
......
XmlSerializer ser = new XmlSerializer (typeof(Lista));
FileStream OStream = new FileStream (filename,
FileMode.Create);
OStream.Close();
......
Conjuntos de datos
// -------------------------------------------
// Generación de un documento XML y su esquema
// XSD a partir de un DataSet
// -------------------------------------------
myDataSet.Namespace = "http://elvex.ugr.es/";
myDataSet.DataSetName = "datos";
myDataSet.WriteXml("fichero.xml");
myDataSet.WriteXmlSchema("fichero.xsd");
myDataSet.ReadXml("fichero.xml");
collection.Add("http://elvex.ugr.es/","fichero.xsd");
vr.Schemas.Add(collection);
vr.ValidationType = ValidationType.Schema;
reader.ValidationEventHandler +=
new ValidationEventHandler (this.ValidationEventHandle);
doc.Load(vr);
Crear un sitio web es bastante fácil. Sólo se necesita un servidor web que atienda peticiones
HTTP y un conjunto de ficheros HTML con información. El inconveniente de los ficheros HTML
es que son estáticos, mientras que generalmente nos interesa algo más que mostrar siempre
la misma información (y actualizar a mano periódicamente los ficheros HTML no parece una
gran idea).
Aplicaciones web
En el caso del estándar CGI (Common Gateway Interface), se escriben programas estándar en
línea de comandos que aceptan una serie de parámetros y generan un fichero HTML en su
canal de salida estándar (stdout en el caso de C), lo que no facilita demasiado nuestro trabajo
a la hora de construir aplicaciones de cierta envergadura.
En el IIS (Internet Information Server), el servidor web de Microsoft, las DLLs de ISAPI
(Internet Services Application Programming Interface) mejoran el rendimiento de los
programas CGI y poco más.
Las tecnologías que nos permiten incorporar fragmentos de código a nuestras páginas (tipo
ASP o JSP tradicional) resultan algo más cómodas para el programador, que puede centrarse
en la lógica de su aplicación sin tener que preocuparse en exceso de los detalles de HTTP y
HTML. No obstante, el diseño de las aplicaciones resultantes suele no ser demasiado elegante,
pues tiende a mezclar la interfaz de usuario con la lógica de la aplicación.
En cualquier caso, en las aplicaciones web, el navegador del cliente se limita a mostrar una
página HTML estándar que habrá sido generada dinámicamente en el servidor y, por tanto, el
cliente será independiente de la tecnología utilicemos en el servidor. El cliente en una
aplicación web se limita a mostrar la página tal cual le llega (thin client).
Servicios web
Los servicios web, básicamente, establecen un lenguaje común para el intercambio de datos:
XML (eXtensible Markup Language). Este lenguaje común permite que distintos sistemas
pueden comunicarse entre sí de una forma sencilla y, de esta forma, se facilita la construcción
de sistemas heterogéneos:
En el cliente
En principio, una aplicación web se puede desarrollar de forma que todo el trabajo lo realice el
servidor web y el usuario final sólo necesite un navegador web, si bien esta opción no resulta
demasiado atractiva por las limitaciones de los formularios HTML. Debido a estas limitaciones,
han surgido numerosas alternativas que permiten ejecutar código en el cliente:
DHTML/JavaScript, COM (controles ActiveX), applets Java, plug-ins específicos (p.ej. Adobe
Flash)...
Estas tecnologías para el cliente permiten mejorar la escalabilidad de las aplicaciones (ya que
se realiza menos trabajo en el servidor), así como la productividad del usuario final (al permitir
la construcción de interfaces de usuario más sofisticadas). Además, el uso de estas tecnologías
"facilita" la construcción de aplicaciones más atractivas de cara al mercado y al usuario final
(interfaces con drag&drop, presentaciones y animaciones Flash...).
Entre las ventajas del software desarrollado de esta forma destacan su accesibilidad (desde
cualquier punto de Internet), su facilidad de mantenimiento (no hay que distribuir el código de
las aplicaciones ni sus actualizaciones), su seguridad (el código no puede manipularlo el
usuario) y su escalabilidad (utilizando "granjas" de servidores y arquitecturas multicapa).
Tecnologías
CGI (Common Gateway Interface)
ISAPI (Internet Server API) @ Microsoft IIS
NSAPI (Netscape Server API)
ASP (Active Server Pages) & ASP.NET (para la plataforma .NET)
JSP (Java Server Pages) & servlets
PHP (Personal Home Page)
ColdFusion (CFM)
Ruby on Rails
Django (Python)
GWT (Google Web Toolkit)
...
Una página ASP no es más que un fichero HTML con extensión .asp (.aspx en el caso de
ASP.NET). Cuando alguien accede a la página, en el servidor se procesa el código que aparece
en la página ASP. El resultado de la ejecución de este código, junto con la parte estática de la
página, es lo que se devuelve al navegador del cliene como una página web más (de ahí el AS
de ASP).
La tecnología ASP, igual que JSP en Java y otras muchas alternativas, incluye un conjunto de
objetos intrínsecos que proporcionan distintos servicios útiles en el desarrollo de aplicaciones
web (Request, Response, Server, Application, Session), además de facilidades para acceder a
componentes COM (p.ej. uso de ADO [ActiveX Data Objects] para acceder a bases de datos).
Las páginas ASP se pueden escribir utilizando distintos lenguajes interpretados (usualmente
una variante de Visual Basic conocida como VBScript), por lo que basta con escribir la página,
guardarla y acceder a ella a través del Internet Information Server (el servidor HTTP de
Microsoft), sin tener que compilarla previamente.
El siguiente ejemplo muestra cómo se pueden crear páginas ASP que generen su contenido
dinámicamente:
<html>
<head>
<title>Hora.asp</title>
</head>
<body>
<h2> Hora actual </h2>
<% Response.Write(Now()) %>
</body>
</html>
El código que aparece entre las etiquetas <% y %> se interpreta en el servidor.
Response.Write sirve para escribir algo en la página HTML de salida y Now() es una función
que devuelve la fecha y hora actuales.
El ejemplo anterior podemos modificarlo un poco para que el usuario pueda visualizar la hora
actual en el momento en que le interese, no sólo cuando carga la página por primera vez:
<html>
<head>
<title>Hora.Request.asp</title>
</head>
<body>
<form method="post">
<input type="submit" id=button name=button
value="Pulse el botón para consultar la hora"
/>
<%
if (Request.Form("button") <> "") then
Response.Write "<p>La hora actual es " & Now()
end if
%>
</form>
</body>
</html>
En esta ocasión, hemos utiizado el objeto Request que nos permite aceptar datos de entrada
en nuestra página ASP.
Para comprobar que los ejemplos anteriores funcionan, sólo hay que guardar los ficheros ASP
correspondientes en algún sitio que cuelque del directorio wwwroot del IIS y acceder a él
desde el navegador utilizando una URL de la forma http://localhost/... (o
http://127.0.0.1/... si no tenemos definido el nombre localhost en el fichero
\WINDOWS\system32\drivers\etc\hosts de nuestra máquina).
Las versiones de ASP anteriores a ASP.NET (ASP Clásico, como algunos las llaman) requieren
escribir bastante código, como por el ejemplo el necesario para mantener el estado de la
página. Este código, además, es poco legible y difícil de mantener al estar mezclado con el
HTML de la interfaz de usuario. En ASP, el fragmento de código ha de colocarse exactamente
en el sitio donde queremos que su salida aparezca, haciendo imposible separar la interfaz de la
lógica de la aplicación. Este hecho dificulta la reutilización de código y complica el
mantenimiento de las aplicaciones web. Además, el ASP Clásico presenta algunos
inconvenientes a la hora de implantar sistemas reales (configuración, eficiencia, depuración...),
muchos de los cuales provienen de las limitaciones de los lenguajes interpretados que se
utilizan para escribir las macros ASP.
En ASP.NET, las aplicaciones web se suelen desarrollar utilizando formularios web, que están
diseñados para hacer la creación de aplicaciones web tan sencilla como la programación en
.NET de aplicaciones para Windows (usando C# o Visual Basic .NET).
Para hacernos una idea de cómo es ASP.NET, retomemos el ejemplo de la sección anterior,
que en ASP.NET queda como sigue si empleamos el lenguaje de programación C#:
Para probar el ejemplo anterior sólo tenemos que guardar la página con la extensión .aspx y
acceder a ella a través del IIS, si lo tenemos instalado en nuestro PC. El Visual Studio 2005
incorpora un "servidor de desarrollo ASP.NET", por lo que no necesitamos disponer del IIS. Nos
basta con crear un nuevo "Sitio Web" (Archivo > Nuevo > Sitio Web... > Sitio Web ASP.NET) o
un proyecto de tipo "Aplicación Web ASP.NET" (Archivo > Nuevo > Proyecto... > Visual C# >
Aplicación Web ASP.NET).
ASP.NET está construido sobre la plataforma .NET y se ejecuta sobre Internet Information
Server (IIS), el servidor web de Microsoft (o bien sobre el servidor de desarrollo que trae
incorporado el Visual Studio 2005):
La programación en ASP.NET está basada en el uso de controles y eventos (como en cualquier
entorno de programación visual para Windows), no en aceptar datos de entrada y generar la
salida en HTML (como ASP), lo que le proporciona un mayor nivel de abstracción, requiere
menos código y permite crear aplicaciones mejor modularizadas, más legibles y, por tanto,
más fáciles de mantener.
En ASP.NET 2.0, se ha de utilizar el atributo CodeFile para especificar el nombre del archivo
de código fuente (junto con el atributo Inherits para especificar el nombre completo de la
clase asociada al formulario web). El atributo CodeBehind se mantiene únicamente para
asegurar la compatibilidad con las versiones anteriores de ASP.NET.
Como es lógico, lo anterior lo haremos normalmente con la ayuda del entorno de desarrollo,
que se encargará de rellenar muchos huecos de forma automática para que nos podamos
centrar en la funcionalidad de nuestra aplicación...
Formularios web
Formularios
Visual Studio .NET proporciona un entorno de programación visual orientado a objetos
mediante el cual podemos crear aplicaciones web utilizando componentes ASP.NET (por
derivación y por composición). Esto nos permite desarrollar aplicaciones web prestando poca
atención al HTML en sí, ya que este no es más que el mecanismo a través del cual los distintos
controles de nuestra interfaz se presentan al usuario final de nuestras aplicaciones.
ASP.NET, en realidad, es una DLL ISAPI que encapsula al Common Language Runtime (CLR)
de la plataforma .NET y permite utilizar ésta en el desarrollo de aplicaciones web para el
Internet Information Server (IIS) de Microsoft:
Las aplicaciones web ASP.NET están formadas por formularios web, que usualmente se dividen
en un fichero .aspx, en el que se especifica la interfaz del formulario, y un fichero de código
aparte .aspx.cs, en el que se implementa la lógica de la aplicación. A partir de la versión 2.0
de la plataforma .NET, que permite la implementación parcial de clases (con la palabra
reservada partial), el fichero de código se divide en dos: un fichero .aspx.cs en el que el
programador implementa sus manejadores de eventos y un fichero .aspx.designer.cs en
el que se recoge todo el código generado automáticamente por el Visual Studio. De esta
forma, se elimina una fuente de error bastante común en versiones del Visual Studio
anteriores al VS2005; a saber, el borrado accidental del código generado automáticamente, lo
que provocaba que la aplicación dejase de funcionar correctamente.
Para poder acceder a una aplicación web, basta con poner esos los ficheros necesarios en
algún lugar accesible a través del IIS (el directorio raíz wwwroot, por ejemplo). Al acceder a la
página .aspx, el código se compila automáticamente y se genera un assembly en la caché del
CLR. Si el texto de la página cambia, el código se recompila automáticamente. Si no cambia,
las solicitudes que se reciban a continuación utilizarán directamente la versión compilada que
se halla en la caché, lo que mejora notablemente la eficiencia de las aplicaciones web ASP.NET
con respecto a las versiones previas de ASP.
NOTA: En el Visual Studio 2005, podemos usar el servidor web de desarrollo que lleva
integrado para probar nuestras aplicaciones sin tener que recurrir al IIS.
Fichero .aspx
<% @Page Language="C#" Inherits="TodayPage" Src="Today.cs" %>
<html>
<body>
<h1 align="center">
Hoy es <% OutputDay(); %>
</h1>
</body>
</html>
Fichero .cs
using System;
using System.Web.UI;
En el fichero .aspx se puede incluir código, si bien lo habitual será utilizar controles
predefinidos. Técnicamente, la página .aspx hereda de la clase definida en el fichero de
código, la cual a su vez hereda de System.Web.UI.Page. De forma que basta con definir los
métodos de esta clase como protected para poder acceder a ellos desde el fichero .aspx.
Controles
Las aplicaciones web ASP.NET, usualmente, emplean controles predefinidos de la biblioteca de
clases de la plataforma .NET. Estos controles proporcionan un modelo orientado a objetos de
los formularios web ASP.NET.
Los controles se indican en el fichero .aspx utilizando etiquetas de la forma <asp:... />,
mientras que la lógica de la aplicación se programa especificando la respuesta de nuestra
interfaz a los distintos eventos que puedan producirse (exactamente igual que en cualquier
entorno de programación visual). El servidor web se encargará de interpretar las etiquetas
correspondientes a los controles ASP.NET para que éstos se visualicen correctamente en el
navegador del usuario. Nosotros no tenemos que preocuparnos de cómo generar el documento
HTML que se visualiza en el navegador web del usuario.
Los controles anteriores se pueden agrupar para construir controles definidos por el
usuario, que nos permitirán reutilizar con comodidad fragmentos de nuestra interfaz de
usuario.
Controles HTML
Las etiquetas HTML estándar, por defecto, se tratan como texto en el servidor y se envían tal
cual al cliente. Para hacerlas programables hay que añadirles un atributo runat="server".
En el siguiente ejemplo podemos ver cómo podemos hacer que un enlace HTML (control
HtmlAnchor) apunte dinámicamente a la URL que nos convenga: sólo tenemos que establecer
un valor adecuado para su propiedad HRef en el código asociado a alguno de los eventos de la
página ASP.NET (p.ej. Page_Load):
En la página ASP.NET:
<html>
...
<body>
<form id="HTMLControl" method="post" runat="server">
<a id="enlace" runat="server">¡Visite nuestra página!</a>
</form>
</body>
<html>
En el fichero de código que hay detrás:
public class HTMLControl : System.Web.UI.Page
{
protected System.Web.UI.HtmlControls.HtmlAnchor enlace;
En Visual Studio .NET, para poder utilizar un control HTML en el servidor sólo tenemos
que seleccionar la opción "Ejecutar como control del servidor" en el menú contextual
asociado a la etiqueta HTML en el diseñador de formularios web. Esto hace que se
añada la declaración correspondiente a la clase que define nuestro formulario, con lo
cual ya podemos programar el comportamiento del control HTML.
Todos los controles en una página ASP.NET deben estar dentro de una etiqueta <form> con el
atributo runat="server".
Además, ASP.NET requiere que todos los elementos HTML estén correctamente anidados y
cerrados (como en XML). De hecho, una página ASP.NET es un fichero XHTML (Extensible
HyperText Markup Language), un estándar de HTML compatible con la sintaxis de XML.
Etiqueta
Control HTML Descripción
HTML
HtmlAnchor <a> Enlace
HtmlButton <button> Botón
HtmlForm <form> Formulario
Cualquier elemento HTML no
HtmlGenericControl cubierto por un control HTML
específico
HtmlImage <image> Imagen
Distintos tipos de entradas en un
formulario HTML: botones
("button", "submit" y "reset"), texto
<input
HtmlInput... ("text" y "password"), opciones
type="...">
("checkbox" y "radio"), imágenes
("image"), ficheros ("file") y
entradas ocultas ("hidden").
HtmlSelect <select>
<table>
HtmlTable... Tablas, filas y celdas
<tr> <td>
HtmlTextArea <textarea>
Controles web
Los controles web corresponden las etiquetas ASP.NET <asp:...> y, como es lógico, también
requieren el atributo runat="server" para funcionar. La sintaxis de las etiquetas ASP.NET es
la siguiente:
donde control especifica el tipo de control web (etiquetas, botones, listas, etc.) e
identificador especifica el identificador que le asociamos a la variable mediante la cual
accederemos al control en nuestro código.
Creamos una aplicación web ASP.NET con un formulario web al que denominamos
WebControl.aspx. A continuación, añadimos un botón usando el control Button
que aparece en la sección "Web Forms" del "Cuadro de herramientas". Esto da lugar a
algo similar a lo siguiente en la página ASP.NET:
...
<form id="WebControl" method="post" runat="server">
<asp:Button id="Button" runat="server" Text="Pulse el
botón"></asp:Button>
</form>
...
Haciendo doble click sobre el botón podemos especificar la respuesta de este control
al evento que se produce al pulsar el botón:
private void Button_Click(object sender, System.EventArgs e)
{
Button.Text = "Ha pulsado el botón";
}
Código fuente (Visual Studio .NET 2003)
Código fuente (Visual Studio .NET 2005)
Control Descripción
AdRotator Muestra una secuencia de imágenes (p.ej. banners)
Button Botón estándar
Calendar Calendario mensual
CheckBox Check box (como en los formularios Windows)
CheckBoxList Grupo de check boxes
DataGrid Rejilla de datos
DataList Muestra una lista utilizando plantillas (templates)
DropDownList Lista desplegable
HyperLink Enlace
Image Imagen
ImageButton Botón dibujado con una imagen
Label Etiqueta (texto estático)
LinkButton Botón con forma de enlace
ListBox Lista (como en los formularios Windows)
Literal Texto estático (similar a Label)
Contenedor en el que se pueden colocar otros
Panel
controles
Reserva espacio para controles añadidos
PlaceHolder
dinámicamente
RadioButton Botón de radio (como en los formularios Windows)
RadioButtonList Grupo de botones de radio
Permite mostrar listas de controles (véase la sección
Repeater
"Data Binding")
Table Tabla
TextBox Caja de edición
Muestra un fichero XML o el resultado de una
Xml
transformación XSL
Controles de validación
Los controles de validación son un tipo especial de controles ASP.NET que permiten validar las
entradas de nuestra aplicación web. Cuando la entrada no verifica la condición que le
imponemos a través de un control de validación, se le muestra un mensaje de error al usuario.
La validación de las entradas se realiza automáticamente cuando se pulsa un botón (ya tenga
éste la forma de un botón estándar, Button, de una imagen, ImageButton, o de un enlace,
LinkButton). No obstante, se puede desactivar la validación si establecemos la propiedad
CausesValidation del botón a false.
Por ejemplo, podemos forzar que una entrada esté dentro de un rango
válido de valores con un control de tipo RangeValidator:
ASP.NET permite encapsular un conjunto de controles en un único control .ascx definido por
el usuario ("Control de usuario Web") usando las opciones "Agregar nuevo elemento..." o
"Agregar componente..." que aparecen tanto en el menú Proyecto como en el menú contextual
asociado a un proyecto ASP.NET en el Explorador de Soluciones del Visual Studio.
ImagePhoto.ImageUrl = "contacts/"+contact.ImageURL;
}
}
...
}
Esto nos permite reutilizar el control en la creación de distintas páginas ASP.NET sin tener que
repetir todos los detalles asociados a la implementación del conjunto de de componentes
encapsulados por el control .ascx (p.ej. layout y presentación estándar, validación de
datos...).
Para utilizar el control .ascx en nuestro formulario, podemos utilizar la directiva @Register e
incluir nuestro control como si de un control predefinido se tratase:
En tiempo de diseño, el control aparece como un simple botón en el Visual Studio 2003. En el
Visual Studio 2005 sí podemos ver, en el diseñador de formularios, cómo quedará el control en
tiempo de ejecución.
En cualquiera de los casos, al ejecutar nuestra aplicación, esto es lo que nos encontraremos:
<form runat="server">
</form>
Postbacks
El evento Page_Load se dispara CADA VEZ que se accede a la página. Si lo que queremos es
ejecutar algo SÓLO LA PRIMERA VEZ, podemos emplear la propiedad Page.IsPostBack. Esta
propiedad es false cuando el cliente carga por primera vez la página y true cuando la página
se devuelve al servidor ("post back") al pulsar el usuario un botón del formulario web. El uso
de "postbacks" es una técnica común para manejar los datos de un formulario web y consiste
en enviar los datos del formulario a la misma página que lo generó dinámicamente.
Estado de la página
ASP.NET nos ahorra tener que escribir mucho código al encargarse automáticamente de
mantener el estado de los controles de los formularios web (ViewState).
value="dDwtMjEwNjQ1OTkwMDs7PiTPnxCh1VBUIX3K2htmyD8Dq6oq" />
Formularios
Visual Studio .NET proporciona un entorno de programación visual orientado a objetos
mediante el cual podemos crear aplicaciones web utilizando componentes ASP.NET (por
derivación y por composición). Esto nos permite desarrollar aplicaciones web prestando poca
atención al HTML en sí, ya que este no es más que el mecanismo a través del cual los distintos
controles de nuestra interfaz se presentan al usuario final de nuestras aplicaciones.
ASP.NET, en realidad, es una DLL ISAPI que encapsula al Common Language Runtime (CLR)
de la plataforma .NET y permite utilizar ésta en el desarrollo de aplicaciones web para el
Internet Information Server (IIS) de Microsoft:
Las aplicaciones web ASP.NET están formadas por formularios web, que usualmente se dividen
en un fichero .aspx, en el que se especifica la interfaz del formulario, y un fichero de código
aparte .aspx.cs, en el que se implementa la lógica de la aplicación. A partir de la versión 2.0
de la plataforma .NET, que permite la implementación parcial de clases (con la palabra
reservada partial), el fichero de código se divide en dos: un fichero .aspx.cs en el que el
programador implementa sus manejadores de eventos y un fichero .aspx.designer.cs en
el que se recoge todo el código generado automáticamente por el Visual Studio. De esta
forma, se elimina una fuente de error bastante común en versiones del Visual Studio
anteriores al VS2005; a saber, el borrado accidental del código generado automáticamente, lo
que provocaba que la aplicación dejase de funcionar correctamente.
Para poder acceder a una aplicación web, basta con poner esos los ficheros necesarios en
algún lugar accesible a través del IIS (el directorio raíz wwwroot, por ejemplo). Al acceder a la
página .aspx, el código se compila automáticamente y se genera un assembly en la caché del
CLR. Si el texto de la página cambia, el código se recompila automáticamente. Si no cambia,
las solicitudes que se reciban a continuación utilizarán directamente la versión compilada que
se halla en la caché, lo que mejora notablemente la eficiencia de las aplicaciones web ASP.NET
con respecto a las versiones previas de ASP.
NOTA: En el Visual Studio 2005, podemos usar el servidor web de desarrollo que lleva
integrado para probar nuestras aplicaciones sin tener que recurrir al IIS.
Fichero .aspx
<% @Page Language="C#" Inherits="TodayPage" Src="Today.cs" %>
<html>
<body>
<h1 align="center">
Hoy es <% OutputDay(); %>
</h1>
</body>
</html>
Fichero .cs
using System;
using System.Web.UI;
En el fichero .aspx se puede incluir código, si bien lo habitual será utilizar controles
predefinidos. Técnicamente, la página .aspx hereda de la clase definida en el fichero de
código, la cual a su vez hereda de System.Web.UI.Page. De forma que basta con definir los
métodos de esta clase como protected para poder acceder a ellos desde el fichero .aspx.
Controles
Las aplicaciones web ASP.NET, usualmente, emplean controles predefinidos de la biblioteca de
clases de la plataforma .NET. Estos controles proporcionan un modelo orientado a objetos de
los formularios web ASP.NET.
Los controles se indican en el fichero .aspx utilizando etiquetas de la forma <asp:... />,
mientras que la lógica de la aplicación se programa especificando la respuesta de nuestra
interfaz a los distintos eventos que puedan producirse (exactamente igual que en cualquier
entorno de programación visual). El servidor web se encargará de interpretar las etiquetas
correspondientes a los controles ASP.NET para que éstos se visualicen correctamente en el
navegador del usuario. Nosotros no tenemos que preocuparnos de cómo generar el documento
HTML que se visualiza en el navegador web del usuario.
Los controles anteriores se pueden agrupar para construir controles definidos por el
usuario, que nos permitirán reutilizar con comodidad fragmentos de nuestra interfaz de
usuario.
Controles HTML
Las etiquetas HTML estándar, por defecto, se tratan como texto en el servidor y se envían tal
cual al cliente. Para hacerlas programables hay que añadirles un atributo runat="server".
En el siguiente ejemplo podemos ver cómo podemos hacer que un enlace HTML (control
HtmlAnchor) apunte dinámicamente a la URL que nos convenga: sólo tenemos que establecer
un valor adecuado para su propiedad HRef en el código asociado a alguno de los eventos de la
página ASP.NET (p.ej. Page_Load):
En la página ASP.NET:
<html>
...
<body>
<form id="HTMLControl" method="post" runat="server">
<a id="enlace" runat="server">¡Visite nuestra página!</a>
</form>
</body>
<html>
En el fichero de código que hay detrás:
public class HTMLControl : System.Web.UI.Page
{
protected System.Web.UI.HtmlControls.HtmlAnchor enlace;
En Visual Studio .NET, para poder utilizar un control HTML en el servidor sólo
tenemos que seleccionar la opción "Ejecutar como control del servidor" en el menú
contextual asociado a la etiqueta HTML en el diseñador de formularios web. Esto
hace que se añada la declaración correspondiente a la clase que define nuestro
formulario, con lo cual ya podemos programar el comportamiento del control HTML.
Todos los controles en una página ASP.NET deben estar dentro de una etiqueta <form> con el
atributo runat="server".
Además, ASP.NET requiere que todos los elementos HTML estén correctamente anidados y
cerrados (como en XML). De hecho, una página ASP.NET es un fichero XHTML (Extensible
HyperText Markup Language), un estándar de HTML compatible con la sintaxis de XML.
Etiqueta
Control HTML Descripción
HTML
HtmlAnchor <a> Enlace
HtmlButton <button> Botón
HtmlForm <form> Formulario
Cualquier elemento HTML no cubierto por un
HtmlGenericControl
control HTML específico
HtmlImage <image> Imagen
Distintos tipos de entradas en un formulario HTML:
botones ("button", "submit" y "reset"), texto
<input
HtmlInput... ("text" y "password"), opciones ("checkbox" y
type="...">
"radio"), imágenes ("image"), ficheros ("file") y
entradas ocultas ("hidden").
HtmlSelect <select>
<table>
HtmlTable... Tablas, filas y celdas
<tr> <td>
HtmlTextArea <textarea>
Controles web
Los controles web corresponden las etiquetas ASP.NET <asp:...> y, como es lógico, también
requieren el atributo runat="server" para funcionar. La sintaxis de las etiquetas ASP.NET es
la siguiente:
donde control especifica el tipo de control web (etiquetas, botones, listas, etc.) e
identificador especifica el identificador que le asociamos a la variable mediante la cual
accederemos al control en nuestro código.
Creamos una aplicación web ASP.NET con un formulario web al que denominamos
WebControl.aspx. A continuación, añadimos un botón usando el control Button que
aparece en la sección "Web Forms" del "Cuadro de herramientas". Esto da lugar a algo similar
a lo siguiente en la página ASP.NET:
...
<form id="WebControl" method="post" runat="server">
<asp:Button id="Button" runat="server" Text="Pulse el botón"></asp:Button>
</form>
...
Haciendo doble click sobre el botón podemos especificar la respuesta de este control al evento
que se produce al pulsar el botón:
private void Button_Click(object sender, System.EventArgs e)
{
Button.Text = "Ha pulsado el botón";
}
Código fuente (Visual Studio .NET 2003)
Código fuente (Visual Studio .NET 2005)
Control Descripción
AdRotator Muestra una secuencia de imágenes (p.ej. banners)
Button Botón estándar
Calendar Calendario mensual
CheckBox Check box (como en los formularios Windows)
CheckBoxList Grupo de check boxes
DataGrid Rejilla de datos
DataList Muestra una lista utilizando plantillas (templates)
DropDownList Lista desplegable
HyperLink Enlace
Image Imagen
ImageButton Botón dibujado con una imagen
Label Etiqueta (texto estático)
LinkButton Botón con forma de enlace
ListBox Lista (como en los formularios Windows)
Literal Texto estático (similar a Label)
Panel Contenedor en el que se pueden colocar otros controles
PlaceHolder Reserva espacio para controles añadidos dinámicamente
RadioButton Botón de radio (como en los formularios Windows)
RadioButtonList Grupo de botones de radio
Permite mostrar listas de controles (véase la sección "Data
Repeater
Binding")
Table Tabla
TextBox Caja de edición
Xml Muestra un fichero XML o el resultado de una transformación XSL
Controles de validación
Los controles de validación son un tipo especial de controles ASP.NET que permiten validar las
entradas de nuestra aplicación web. Cuando la entrada no verifica la condición que le
imponemos a través de un control de validación, se le muestra un mensaje de error al usuario.
La validación de las entradas se realiza automáticamente cuando se pulsa un botón (ya tenga
éste la forma de un botón estándar, Button, de una imagen, ImageButton, o de un enlace,
LinkButton). No obstante, se puede desactivar la validación si establecemos la propiedad
CausesValidation del botón a false.
Por ejemplo, podemos forzar que una entrada esté dentro de un rango
válido de valores con un control de tipo RangeValidator:
Cuando los datos introducidos por el usuario son válidos, la aplicación prosigue su ejecución.
Cuando no se verifica alguna condición de validación, se muestra el mensaje de error asociado
y se le vuelve a pedir al usuario que introduzca correctamente los datos de entrada.
ASP.NET permite encapsular un conjunto de controles en un único control .ascx definido por
el usuario ("Control de usuario Web") usando las opciones "Agregar nuevo elemento..." o
"Agregar componente..." que aparecen tanto en el menú Proyecto como en el menú contextual
asociado a un proyecto ASP.NET en el Explorador de Soluciones del Visual Studio.
En el fichero de
código .ascx.cs definimos una subclase de
System.Web.UI.UserControl, con todas las propiedades que deseemos:
if (contact!=null) {
LabelName.Text = contact.Name;
LinkEMail.Text = contact.EMail;
LinkEMail.NavigateUrl = "mailto:"+contact.EMail;
LabelTelephone.Text = contact.Telephone;
LabelMobile.Text = contact.Mobile;
LabelFax.Text = contact.Fax;
LabelAddress.Text = contact.Address.Replace("\n","<br>");
LabelComments.Text = contact.Comments;
ImagePhoto.ImageUrl = "contacts/"+contact.ImageURL;
}
}
...
}
Esto nos permite reutilizar el control en la creación de distintas páginas ASP.NET sin tener que
repetir todos los detalles asociados a la implementación del conjunto de de componentes
encapsulados por el control .ascx (p.ej. layout y presentación estándar, validación de
datos...).
Para utilizar el control .ascx en nuestro formulario, podemos utilizar la directiva @Register e
incluir nuestro control como si de un control predefinido se tratase:
<form runat="server">
...
<user:contactviewer id="ContactView" runat="server" Visible="False">
</user:contactviewer>
...
</form>
También podemos arrastrar el control .ascx directamente del Explorador de Soluciones al
Diseñador de Formularios Web. Curiosamente, los proyectos "Aplicación Web ASP.NET" de
Visual Studio 2005 no permiten arrastrar un control definido por el usuario desde el explorador
de soluciones hasta el diseñador de formularios, algo que sí podremos hacer si hemos creado
nuestro proyecto usando la opción "Nuevo Sitio Web" (¿alguien lo entiende?).
En tiempo de diseño, el control aparece como un simple botón en el Visual Studio 2003. En el
Visual Studio 2005 sí podemos ver, en el diseñador de formularios, cómo quedará el control en
tiempo de ejecución.
En cualquiera de los casos, al ejecutar nuestra aplicación, esto es lo que nos encontraremos:
<form runat="server">
</form>
Postbacks
El evento Page_Load se dispara CADA VEZ que se accede a la página. Si lo que queremos es
ejecutar algo SÓLO LA PRIMERA VEZ, podemos emplear la propiedad Page.IsPostBack. Esta
propiedad es false cuando el cliente carga por primera vez la página y true cuando la página
se devuelve al servidor ("post back") al pulsar el usuario un botón del formulario web. El uso
de "postbacks" es una técnica común para manejar los datos de un formulario web y consiste
en enviar los datos del formulario a la misma página que lo generó dinámicamente.
Estado de la página
ASP.NET nos ahorra tener que escribir mucho código al encargarse automáticamente de
mantener el estado de los controles de los formularios web (ViewState).
value="dDwtMjEwNjQ1OTkwMDs7PiTPnxCh1VBUIX3K2htmyD8Dq6oq" />
HTTP
El protocolo HTTP [HyperText Transfer Protocol] es un protocolo simple de tipo solicitud-
respuesta, de modo que se establece una conexión diferente cada vez que accedemos a una
página:
Cuando tecleamos la dirección de una página, el navegador web establece una conexión TCP
con el servidor (usualmente a través del puerto 80). A continuación, el cliente envía un
mensaje al servidor (solicitud) y éste le responde con otro mensaje (respuesta). Tras esto, la
conexión se cierra y el ciclo vuelve a empezar. No obstante, hay que mencionar que, por
cuestiones de eficiencia (reducción de la congestión en la red), HTTP/1.1 mantiene conexiones
persistentes, lo cual no quiere decir que la interacción entre cliente y servidor varíe desde el
punto de vista lógico.
HTTP sólo distingue dos tipos de mensajes (solicitudes y respuestas) que se diferencian
únicamente en su primera línea. Tanto solicitudes como respuestas pueden incluir distintas
cabeceras (del tipo clave:
GET http://elvex.ugr.es/index.html
If-Modified-Since: Thu, 31 Oct 2002 19:41:00 GMT
HTTP/1.1 200 OK
Server: Microsoft-IIS/5.0
Date: Sun, 17 Aug 2003 10:35:30 GMT
Content-Type: text/html
Last-Modified: Tue, 27 Mar 2001 10:34:52 GMT
Content-Length: XXX
<html>
--- Aquí se envía el texto de la página HTML
</html>
Código Significado
1xx Mensaje informativo
2xx Éxito (vg: 200 OK)
3xx Redirección (vg: 302 Resource temporarily moved)
Error en el cliente (vg: 400 Bad request, 401 Unauthorized, 403
4xx
Forbidden)
5xx Error en el servidor (vg: 500 Internal Server Error)
Cookies
HTTP, por definición, es un protocolo sin estado. Sin embargo, al desarrollar aplicaciones web,
mantener el estado resulta imprescindible. Por ejemplo, en un sistema de comercio electrónico
debemos ser capaces de almacenar de alguna forma el carrito de la compra de un cliente
concreto.
Una primera solución a este problema (no demasiado acertada, por cierto) consiste en
gestionar sesiones utilizando cookies (RFC 2965). Conforme navegamos vamos generando
cookies ["galletitas"] en el cliente que luego se encargará de consumir el servidor. La idea es
utilizar una cookie por servidor o grupo de servidores que se encargue de almacenar en el
cliente la información que pueda necesitar el servidor (p.ej. los artículos que llevamos en el
cesto de la compra).
Obsérvese que los datos asociados a una cookie se almacenan en la máquina cliente
(usualmente sin ningún tipo de protección) y, por tanto, no deberían nunca incluir información
cuya privacidad sería deseable mantener. Además, el uso de cookies lo provoca el servidor y
no el cliente (Set-Cookie2), por lo que en determinadas situaciones puede resultar
conveniente configurar el cliente para que ignore las cookies (no devolviendo la cabecera
Cookie2 que le sirve al servidor para mantener la sesión).
Sesiones en ASP.NET
ASP.NET proporciona un conjunto de objetos que nos permiten gobernar la interacción entre el
cliente y el servidor al desarrollar aplicaciones web. En realidad, todo ASP.NET se construye a
partir del interfaz IHttpHandler:
IHttpHandler {
void ProcessRequest (HttpContext context);
bool IsReusable ();
}
Para escribir nuestra aplicación web bastaría con construir una clase que implementase este
interfaz y utilizar sentencias del tipo context.Response.Write("<html>...");. Como es
lógico, esto no sería mucho mejor que programar CGIs y nosotros aprovecharemos los
distintos objetos que nos proporciona ASP.NET para facilitarnos el trabajo. ASP.NET se
encargará de compilar nuestra página construyendo una clase derivada de
System.Web.UI.Page, la cual implementa la interfaz IHttpHandler
Los objetos más importantes con los que trabajamos en ASP.NET son los siguientes:
Objeto Representa...
HttpContext El entorno en el que se atiende la petición
Request La petición HTTP realizada por el cliente
Response La respuesta HTTP devuelta por el servidor
Server Algunos métodos útiles
Los dos últimos objetos son los que nos permiten manejar con comodidad las sesiones de
usuario en ASP.NET y mantener el contexto en el que un usuario interactúa con una aplicación
web. Mientras el usuario siempre interactúe con la misma página, ASP.NET se encarga de
mantener el estado de ésta de forma automática. No obstante, en cuanto el usuario cambie de
página (algo habitual en cualquier aplicación web real), tendremos que almacenar la
información de su sesión de alguna forma:
En el caso de la colección Session, ésta se comparte entre todas las solicitudes de clientes
que utilicen el mismo identificador de sesión. Dicho identificador se genera automáticamente
cuando el cliente accede por primera vez a la aplicación web y se transmite desde el cliente
cada vez que éste vuelve acceder a una página de la aplicación web.
http://servidor/aplicación/(uqwkag45e35fp455t2qav155)/página.aspx
Para acceder a la información de la sesión no tenemos más que utilizar la propiedad Session
de nuestra página ASP.NET, ya que Session está definida en System.Web.UI.Page, la clase
de la que derivan todas las páginas ASP.NET:
Utilizando las colecciones Application y Session, por ejemplo, podemos añadirle a una
página ASP.NET un "contador" de visitas que nos permita contabilizar el tráfico que recibe
nuestra página (saber cuántas veces se visualiza la página y cuántas veces accede a la página
un mismo usuario en una única sesión):
En primer lugar, inicializamos los contadores que usaremos en el fichero Global.asax (o
Global.asax.cs):
A continuación, nos asegurarnos de que, cada vez que se accede a la página, los contadores
contabilizan el acceso a la página. Esto lo podemos hacer, por ejemplo, como respuesta al
evento Page_Load (respuesta que definiremos en el fichero aspx.cs asociado a nuestra
página):
A diferencia de ASP, ASP.NET no requiere cookies y, aún mejor, permite configurar la forma en
la que se gestionan las sesiones en la sección <sessionState ... /> del fichero de
condiguración Web.config (que se crea automáticamente en Visual Studio 2003 pero hemos
de añadirlo explícitamente en Visual Studio 2005). Este fichero de configuración nos permite,
entre otras cosas, no tener que modificar el código de nuestra aplicación cuando, en vez de un
único servidor, utilizamos un cluster y no queremos forzar la afinidad de un cliente a un
servidor fijo.
Aunque la seguridad sea generalmente un tema obviado por la mayor parte de los
programadores, en una aplicación web resulta un aspecto esencial. Planificar los mecanismos
necesarios de seguridad para evitar accesos no autorizados a nuestras aplicaciones y servicios
web se convierte, por tanto, en algo que todo programador debería saber hacer
correctamente.
Cuando hablamos de seguridad en las aplicaciones web realizadas con la plataforma .NET, en
realidad estamos hablando acerca de cómo restringir el acceso a determinados recursos
gestionados por el Internet Information Server (IIS) de Microsoft. Sin entrar en detalles de
interés para los aficionados a las técnicas criptográgicas de protección de datos, a continuación
veremos cómo implementar mecanismos de identificación de usuarios a través de contraseñas
para acceder a sitios web desarrollados con páginas ASP.NET.
Autentificación y autorización en IIS
El funcionamiento habitual de una aplicación web que requiere la autentificación del usuario es
redirigir a éste a un formulario de login cuando intenta acceder a un área restringida de
nuestra aplicación web. Para lograrlo, nos basta con modificar un fichero de configuración y el
IIS se encargará de hacerlo por nosotros.
<configuration>
<system.web>
<authentication mode="Forms">
<forms loginUrl="login.aspx" name=".ASPXFORMSAUTH"></forms>
</authentication>
<authorization>
<deny users="?" />
</authorization>
</system.web>
</configuration>
Cuando selecccionamos el modo de autentificación Forms hemos de indicar también cuál será
el formulario encargado de que nuestros usuarios se identifiquen al usar el sistema
(login.aspx) en el ejemplo de arriba.
Por ejemplo, si en una parte de nuestra aplicación queremos que cualquier persona pueda
acceder (incluso sin identificarse), basta con incluir la siguiente autorización en el fichero de
configuración correspondiente:
<authorization>
<allow users="*" />
</authorization>
<authorization>
<allow users="administrador" />
<deny users="*" />
</authorization>
Incluso podríamos haber puesto una lista de nombres de usuario separados por comas.
Podemos crear una aplicación web a la cual sólo tendrán acceso algunos usuarios
de nuestra máquina si utilizamos "Windows" como mecanismo de autentificación
y restringimos el acceso anónimo al directorio donde alojamos la aplicación en IIS
(Panel de control > Herramientas administrativas > Administración de equipos >
Servicios y Aplicaciones > Servicios de Internet Information Server):
Una vez hecho esto, modificamos el fichero Web.config de acuerdo a nuestras
necesidades y, cuando intentemos acceder a la aplicación nos aparecerá una
ventana como la siguiente:
Cuando un usuario intente acceder a una página cuyo acceso esté restringido, el usuario será
redirigido a un formulario específico de login, que, al menos, incluirá dos campos para que el
usuario indique su nombre y su clave (dos controles de tipo TextBox, especificando la
propiedad TextMode=Password para la caja de edición correspondiente a la contraseña de
acceso). En dicho formulario para comprobaremos el identificador y la clave del usuario:
if ( textBoxID.Text.Equals("usuario") &&
textBoxPassword.Text.Equals("clave") ) {
FormsAuthentication.RedirectFromLoginPage(textBoxID.Text,false);
} else {
// Error de autentificación...
}
Cuando el usuario se identifica correctamente, lo único que hacemos es indicarle al IIS que le
permita al usuario acceder a la página a la que inicialmente deseaba acceder. Para ello
utilizamos un método de la clase FormsAuthentication que está incluida en el espacio de
nombres System.Web.Security. El IIS se encargará de todo lo demás por nosotros.
Creamos una aplicación web con dos formularios, uno de los cuales lo utilizaremos
como formulario de identificación:
<authentication mode="Forms">
<forms loginUrl="Login.aspx" name=".ASPXFORMSAUTH"></forms>
</authentication>
<authorization>
<deny users="?" />
</authorization>
El servicio de Internet más utilizado es la World Wide Web (WWW). Para escribir documentos
para la web se utiliza el lenguaje HTML [HyperText Markup Language].
Un documento HTML es un fichero de texto plano (ASCII) que incluye ciertas marcas o
etiquetas [tags] que le indican al navegador, entre otras cosas, cómo debe visualizarse el
documento. Los tags se escriben encerrados entre ángulos ("<" y ">") y usualmente van por
parejas ("<tag>" y "</tag>").
Un documento HTML, delimitado por la pareja de etiquetas <HTML> y </HTML>, tiene dos partes
principales:
p.ej.
<HTML>
<HEAD>
<TITLE> Título del documento </TITLE>
<META NAME="Author" CONTENT="Fernando Berzal">
<META NAME="Keywords" CONTENT="Internet, HTML">
<META NAME="Description" CONTENT="Introducción al uso
de Internet">
</HEAD>
<BODY>
Cuerpo del documento...
</BODY>
</HTML>
Párrafos (<P>)
Encabezados para definir títulos y subtítulos (de <H1> a <H6>, de mayor a menor
nivel)
Hiperenlaces: <A HREF="url">Texto</A>
Imágenes (GIF o JPG): <IMG SRC="fichero" BORDER=0 ALT="Texto
descriptivo">
Listas numeradas (<OL> ... </OL>) o no numeradas (<UL> ... </UL>) cuyos
elementos se indican con la etiqueta <LI>
Caracteres especiales
Las vocales acentuadas, las eñes y otros caracteres "no estándar" (incluyendo los ángulos que
se utilizan para las etiquetas HTML) requieren secuencias especiales de caracteres para
representarlos. La siguiente tabla recoge algunas de ellas:
Tablas
Las tablas se delimitan con las etiquetas <TABLE> y </TABLE>. Entre estas dos etiquetas se han
de incluir una serie de filas delimitadas por <TR> y </TR>. Cada fila, a su vez, incluye una serie
de celdas <TD> y </TD>. Por ejemplo:
<TABLE border=2>
<TR bgcolor="#ccccee">
<TH COLSPAN=2> <IMG SRC="cogs.gif"> Tabla en
HTML </TH>
</TR>
<TR bgcolor="#e0e0e0">
<TH> Datos</TH>
<TH> Valores</TH>
</TR>
<TR>
<TD> Dato 1</TD>
<TD> Valor 1</TD>
</TR>
<TR>
<TD> Dato 2</TD>
<TD> Valor 2</TD>
</TR>
<TR>
<TD> Dato 3</TD>
<TD> Valor 3</TD>
</TR>
</TABLE>
Tabla en HTML
Datos Valores
Dato 1 Valor 1
Dato 2 Valor 2
Dato 3 Valor 3
Formularios
HTML también permite que el usuario no se limite a leer el contenido de la página, sino que
también puede introducir datos mediante formularios (la base del comercio electrónico). Por
ejemplo, el siguiente formulario:
<FORM METHOD="POST"
ACTION="mailto:berzal@acm.org">
<INPUT TYPE="text" NAME="NOMBRE" SIZE=30
MAXLENGTH=40>
<TEXTAREA NAME="COMENTARIOS" ROWS=6 COLS=40>
</TEXTAREA>
<INPUT TYPE="submit" VALUE="Enviar sugerencias
por e-mail">
</FORM>
Nombre
Comentarios
Enviar sugerencias por e-mail
TYPE="IMAGE"...>), de tal forma que la acción que se realice pueda depender de la zona de la
imagen que seleccione el usuario.
Hojas de estilo
Para emplear una hoja de estilo en la presentación de nuestra página web, sólo tenemos que
incluir la siguiente etiqueta antes del cuerpo de nuestro documento HTML:
donde style.css es el fichero que contiene la hoja de estilo que se empleará para visualizar el
documento HTML.
El texto de la hoja de estilo ha de escribirse de acuerdo a la siguiente sintaxis:
ETIQUETA {
propiedad1: valor1;
propiedad2: valor2;
}
ETIQUETA1, ETIQUETA2 {
propiedad: valor;
}
.CLASE {
propiedad: valor;
}
donde las etiquetas y las clases son las que se utilican en los documentos HTML, mientras que
las propiedades aplicables a cada elemento y los valores que pueden tomar dichas propiedades
están definidas en un estándar emitido por el W3C.
Por ejemplo, el cuerpo de esta página se visualiza con el siguiente estilo para dejar márgenes
y mostrar una imagen de fondo:
BODY
{
background-image:
url(http://elvex.ugr.es/decsai/internet/image/internet.jpg);
color: #000000;
margin-left: 10%;
margin-right: 10%;
margin-top: 5%;
margin-bottom: 5%;
}
De hecho, el ejemplo que acabamos de ver se muestra utilizando un estilo especial (definido
con la clase example) de la siguiente forma:
En el documento HTML:
<table class="example">
...
En la hoja de estilo CSS:
.example
{
background-color: #e0e0e0;
}
Además, todo el texto de este documento aparece justificado a ambos márgenes porque en la
hoja de estilo hemos escrito lo siguiente:
P, BLOCKQUOTE, LI, TD
{
text-align: justify;
}
Por otro lado, también hemos modificado la forma en la que se visualizan los enlaces en una
página (y cómo cambian al pasar el cursor del ratón sobre ellos, al menos si utilizamos como
navegador el Internet Explorer de Microsoft):
A
{
text-decoration: none;
}
A:hover
{
color: #009999;
}
Jugando un poco con las posibilidades que nos ofrecen las hojas de estilo CSS se puede
conseguir que nuestras páginas HTML estándar tengan buen aspecto sin tener que plagarlas de
etiquetas auxiliares como FONT, las cuales lo único que consiguen es que el texto de nuestro
documento HTML sea menos legible y más difícil de mantener.
Información adicional
Programación concurrente
Programación concurrente
Se entiende por programación concurrente el conjunto de técnicas y notaciones que sirven
para expresar el paralelismo potencial en los programas, así como resolver problemas de
comunicación y sincronización.
Una aplicación concurrente está formada por un conjunto de procesos concurrentes. En ella
existen distintas hebras de control independientes, vías simultáneas de ejecución. Dichas
hebras de control pueden ser procesos independientes en el sistema operativo o hebras dentro
de un proceso. Una aplicación multihebra está constituida por distintas hebras que comparten
el espacio de un proceso en el sistema operativo.
Consideraciones
El diseño de aplicaciones concurrentes es más complejo que el de aplicaciones
secuenciales, ya que hemos de descomponer el programa en un conjunto
de tareas más o menos independientes con el fin de aprovechar el
paralelismo que pueda existir. Si no existe ese paralelismo potencial, no
tiene sentido que intentemos descomponer nuestra aplicación en tareas
independientes.
La implementación de aplicaciones concurrentes es también más compleja
que la de aplicaciones secuenciales convencionales porque hemos de
garantizar la coordinación de las distintas hebras o procesos con los
mecanismos de comunicación adecuados, además de velar por la integridad
de los datos con los que éstas trabajan simultáneamente (para lo cual
hemos de sincronizar el acceso a los mismos).
La depuración de las aplicaciones concurrentes es extremadamente difícil,
dado que la ejecución de los distintos procesos/hebras se realiza de forma
independiente y las operaciones que realizan se pueden entrelazar de
cualquier forma en función de cómo les asigne la CPU el sistema operativo.
Cada hebra/proceso supone una carga adicional para el sistema (p.ej. los
cambios de contexto son costosos, especialmente en el caso de los
procesos). Hay que tener en cuenta la eficiencia de la implementación
resultante, que puede medirse en función del tiempo de respuesta del
sistema o de la cantidad de trabajo que realiza por unidad de tiempo
[throughput].
¿Por qué usar hebras y procesos?
El uso de paralelismo (múltiples procesos y hebras, en particular) proporciona una serie de
ventajas frente a las limitaciones de los sistemas monotarea.
Ejemplo
Supongamos que nuestra aplicación tiene que ocuparse de la
realización de copias de seguridad de los datos con los que trabaja.
Con una única hebra tendríamos que programar las copias de
seguridad fuera del horario habitual de trabajo (¿y si tiene que
funcionar las 24 horas del día?). Con varias hebras, podemos
aprovechar los períodos de inactividad del sistema.
El diseño correcto de una aplicación concurrente puede permitir que un programa complete
una mayor cantidad de trabajo en el mismo período de tiempo (como sucedía en el ejemplo
antes mencionado) pero también sirve para otros menesteres más mundanos, desde la
creación de interfaces que respondan mejor a las órdenes del usuario hasta la creación de
aplicaciones que den servicio a varios clientes (como puede ser cualquier aplicación web a la
que varios usuarios pueden acceder simultáneamente).
En la interfaz de usuario
Cuando una aplicación tiene que realizar alguna tarea larga, su interfaz debería seguir
respondiendo a las órdenes que el usuario efectúe.
Establecimiento de prioridades
Como es lógico, se le asigna mayor prioridad a las tareas más
importantes (vg: las que requieran una respuesta más rápida).
Cuando se utiliza una sola hebra, el programa debe detener completamente la ejecución
mientras espera a que se realice cada tarea. La CPU permanece ocupada completamente (o
inactiva) hasta que el proceso actual termine. Si se utilizan varias hebras, el sistema puede
usarse para realizar varias tareas simultáneamente (vg: reproducción de MP3s en
background).
Paralelismo real
En un sistema multiprocesador, si la aplicación se descompone en
varias hebras, el sistema operativo podrá asignar cada una a una de
las CPUs del sistema.
Modularización: Paralelismo implícito
En muchas ocasiones, un programa puede diseñarse como varios procesos paralelos que
funcionen de forma independiente. La descomposición de una aplicación en varias hebras
puede resultar muy beneficiosa:
IMPORTANTE
El objetivo principal del uso de paralelismo es mejorar el rendimiento
del sistema. El diseñador/programador deberá decidir hasta qué punto
debe utilizarse en cada momento.
En cualquier caso, el uso de paralelismo, y de hebras en particular, es más común de lo que
podría pensarse en un principio (y menos de lo que debería).
Procesos
Ejecución de procesos
La clase System.Diagnostics.Process permite crear y monitorizar procesos (accediendo a
la información que se visualiza en el Administrador de Tareas de Windows).
Ejecución de procesos
Lanzar proceso:
System.Diagnostics.Process.Start("iexplore.exe");
System.Diagnostics.Process.Start("iexplore.exe", args);
using System.Diagnostics;
...
ProcessStartInfo info = new ProcessStartInfo();
info.FileName = "iexplore.exe";
info.Arguments = "http://elvex.ugr.es/decsai/CSharp/";
info.WindowStyle = System.Diagnostics.ProcessWindowStyle.Maximized;
Process.Start(info);
openFileDialog.InitialDirectory = "c:\\" ;
openFileDialog.Filter = "txt files (*.txt)|*.txt|All files
(*.*)|*.*";
openFileDialog.FilterIndex = 2 ;
openFileDialog.RestoreDirectory = true ; // Vuelve al directorio
actual
if (openFileDialog.ShowDialog() == DialogResult.OK) {
System.Diagnostics.Process.Start(openFileDialog.FileName);
}
System.Diagnostics.Process.Start("http://elvex.ugr.es/decsai/Csharp/");
using System.Diagnostics;
...
ProcessStartInfo info = new ProcessStartInfo();
info.FileName = "Ade.jpg";
info.WorkingDirectory = "f://";
info.Verb = "edit"; // vs. "open" || "print"
Process.Start(info);
// Verbos comunes
// --------------
// open Abre un ejecutable, documento o carpeta
// edit Edita un documento
// print Imprime un documento
// explore Explora una carpeta
// find Inicia una búsqueda en el directorio especificado
Descargar código fuente (Visual Studio 2003)
Descargar código fuente (Visual Studio 2005)
proceso.WaitForExit();
proceso.Close();
proceso.StartInfo.FileName = "cmd.exe";
proceso.StartInfo.Arguments =
"/C dir \""+path+"\" >> \"" + salida +"\" && exit";
proceso.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
proceso.StartInfo.CreateNoWindow = true;
proceso.Start()
if (!proceso.HasExited()) {
proceso.Kill(); // Finalizamos el proceso
} else {
...
}
Además, también podemos detectar cuándo finaliza un proceso que hayamos lanzado si
empleamos el manejador de eventos Exited de la clase Process. De esta forma, no tenemos
por qué bloquear la ejecución de nuestra aplicación mientras esperamos la terminación de un
proceso:
proces.Close();
}
La clase Process también nos permite provocar la terminación de un proceso. Ésta se puede
realizar explícitamente utilizando los métodos Kill() y CloseMainWindow(), siendo este
último el método recomendado, pues equivale a que el usuario de la aplicación cierre ésta de
la forma usual. Es decir, CloseMainWindow solicita cerrar la aplicación como si el propio
usuario cerrase la ventana principal de la aplicación (lo cual puede provocar la aparición de
mensajes de la aplicación), mientras que Kill finaliza el proceso "por las bravas", pudiendo
ocasionar la pérdida de datos que no hayan sido previamente guardados.
Monitor de procesos
listProcesses.Items.AddRange(procesos);
textInfo.Clear();
if (selected!=null)
textInfo.Lines = new string[] {
"Proceso: " + selected.ProcessName,
"PID: " + selected.Id,
// "Máquina: " + selected.MachineName,
"Prioridad: " + selected.PriorityClass,
"Uso de memoria: " + selected.WorkingSet + " bytes",
// selected.PagedMemorySize
// selected.VirtualMemorySize
"Tiempo de CPU: " + selected.TotalProcessorTime,
"Hora de inicio: " + selected.StartTime,
"Módulo principal: " + selected.MainModule.FileName
// selected.MainWindowTitle
};
if (selected!=null)
selected.Kill();
}
if (selected!=null) {
if (selected.Id==Process.GetCurrentProcess().Id)
MessageBox.Show("Ha decidido finalizar la aplicación actual");
selected.CloseMainWindow();
}
}
Descargar código fuente (Visual Studio 2003)
Descargar código fuente (Visual Studio 2005)
Una observación: Process.Start utiliza por defecto la función ShellExecute del API Win32.
Cuando utilicemos redireccionamientos, la propiedad ProcessStartInfo.UseShellExecute
debe estar puesta a "false" antes de invocar al método Process.Start.
proceso.StartInfo.FileName = "cmd.exe";
proceso.StartInfo.UseShellExecute = false;
proceso.StartInfo.CreateNoWindow = true;
proceso.StartInfo.RedirectStandardInput = true;
proceso.StartInfo.RedirectStandardOutput = true;
proceso.StartInfo.RedirectStandardError = true;
proceso.Start()
sIn.Close();
sOut.Close();
sErr.Close();
proceso.Close();
MessageBox.Show(output);
Para los programas que no utilizan StdIn se puede utilizar el método SendKeys para simular la
pulsación de teclas:
proceso.StartInfo.FileName = "notepad";
proceso.StartInfo.WindowStyle = ProcessWindowStyle.Normal;
proceso.EnableRaisingEvents = true;
proceso.Start();
Cualquier combinación de teclas se puede enviar con SendKeys, lo que permite hacer
prácticamente cualquier cosa con una aplicación Windows (siempre que nos aseguremos de
que es la aplicación activa, la que tiene el foco).
El API de Windows incluye una gran cantidad de funciones (no métodos), del orden de
miles. En el caso que nos ocupa, podemos recurrir a las funciones FindWindow y
SetForegroundWindow del API Win32 para establecer la aplicación activa. Como estas
funciones no forman parte de la biblioteca de clases .NET, tenemos que acceder a ellas
usando los servicios de interoperabilidad con COM. Estos servicios son los que nos
permiten acceder a recursos nativos del sistema operativo y se pueden encontrar en el
espacio de nombres System.Runtime.InteropServices.
Para poder usar la función SetForegroundWindow, por ejemplo, tendremos que escribir
algo parecido a lo siguiente:
using System.Runtime.InteropServices;
...
[DllImport("User32",EntryPoint="SetForegroundWindow")]
private static extern bool SetForegroundWindow(System.IntPtr hWnd);
if (proceso.Responding) {
SetForegroundWindow(proceso.MainWindowHandle);
SendKeys.SendWait("Sorpresa!!!");
SetForegroundWindow(this.Handle);
}
La clase Thread
La clase System.Thread representa una hebra del sistema. Las hebras se lanzan cuando se
invoca un delegado sin argumentos que sirve de punto de entrada a la hebra. Al no tener
argumentos este delegado, el estado inicial de la hebra hay que establecerlo previamente en el
objeto que aloje el delegado.
// Inicialización de parámetros
tarea.parameter= 1234;
// Creación de la hebra
// Ejecución de la hebra
thread.Start();
// ...
thread.Join();
Cálculo de PI
Aunque no sea algo especialmente útil en el trabajo cotidiano de un programador,
supongamos que, por algún extraño motivo, hemos de calcular con precisión el valor
del número PI y no nos vale la constante definida en System.Math.PI, que sólo
incluye veinte dígitos de precisión. No obstante, el proceso que seguiremos para
desarrollar una aplicación multihebra será el mismo que el que seguiríamos en
cualquier otro caso real (p.ej. realización de copias de seguridad mientras nuestra
aplicación sigue funcionando, implementación de servidores de aplicaciones, acceso a
recursos a través de la red sin detener la ejecución de nuestra aplicación, etc.).
Recordemos que, en general, la utilización de hebras es siempre útil cuando hemos
de realizar cualquier tarea que requiera su tiempo...
Para calcular PI hasta la precisión deseada sólo tenemos que implementar un bucle y
utilizar una función que nos va devolviendo los dígitos de PI de 9 en 9 (Pi.cs):
private void CalcularPi (int precision)
{
int digitos, calculados, nuevos;
string ds;
StringBuilder pi = new StringBuilder("3.", precision + 2);
ShowProgress(pi.ToString(), 0, precision);
calculados = 0;
while (calculados<precision) {
digitos = NineDigitsOfPi.StartingAt(calculados+1);
nuevos = Math.Min(precision - calculados, 9);
ds = string.Format("{0:D9}", digitos);
pi.Append(ds.Substring(0, nuevos));
piThread.Start();
}
// Declaración
// Creación
// Uso
delegado((int)editDigits.Value);
Cuando un delegado se usa como si fuese una función, en realidad se está llamando al método
síncrono Invoke, que es el que se encarga de llamar a la función concreta con la que se
instancia el delegado (CalcularPi en este caso). Los otros dos métodos del delegado,
BeginInvoke y EndInvoke, son los que permiten invocar al delegado de forma asíncrona. De
forma que podemos calcular PI en una hebra independiente de la siguiente forma:
En principio, todo parece ir bien, aunque aún nos quedan algunos detalles por pulir...
Hebras en aplicaciones Windows
Aunque tuvimos suerte en el ejemplo anterior (posiblemente por la implementación de nuestro
sistema operativo), en realidad violamos una regla básica de Windows: manipular una ventana
únicamente desde la hebra que la crea. En general, como veremos en la siguiente sección del
curso, no es correcto acceder un recurso compartido desde distintas hebras si no utilizamos los
mecanismos de protección adecuados.
La documentación de la plataforma .NET lo deja claro. Sólo hay cuatro métodos de un control
que se pueden llamar de forma segura desde cualquier hebra: Invoke, BeginInvoke,
EndInvoke y CreateGraphics). Cualquier otro método ha de llamarse a través de uno de
los anteriores, como, por ejemplo, los que modifican las distintas propiedades de los controles
de nuestra ventana. En realidad, sólo tenemos que crear un nuevo delegado que se ejecute en
la hebra principal correspondiente a la interfaz gráfica.
El uso de Invoke nos garantiza que accedemos de forma segura a los controles de nuestra
ventana en una aplicación multihebra. La hebra principal crea una hebra encargada de realizar
una tarea computacionalmente costosa [la hebra trabajadora] y ésta le pasa el control a la
hebra principal cada vez que necesita actuar sobre la interfaz. La siguiente figura ilustra cómo
funciona nuestra aplicación en tiempo de ejecución:
Tener que llamar a Invoke cada vez que queremos garantizar el correcto funcionamiento de
nuestra aplicación multihebra es realmente incómodo y, además, resulta bastante fácil que se
nos olvide hacerlo en alguna ocasión. Por tanto, no es mala idea que sea la propia función a la
que llamamos la que se encargue de asegurar su correcta ejecución en una aplicación
multihebra:
...
using System.Threading;
ID = Guid.NewGuid();
// 2. Lanzar la hebra
Response.Redirect("Result.aspx?ID=" + ID.ToString());
}
}
Results.Add(ID, resultado);
}
...
}
Una clase auxiliar Results mantiene los resultados de las distintas hebras para que la
página de resultados pueda acceder a ellos:
using System;
using System.Collections;
if (Results.Contains(ID)) {
// La tarea ha terminado: Mostrar el resultado
lblMessage.Text = Results.Get(ID).ToString();
Results.Remove(ID);
} else {
// Aún no tenemos el resultado: Esperar otros 2 segundos
Response.AddHeader("Refresh", "2");
}
}
...
}
La solución aquí propuesta es extremadamente útil en la práctica. Imagine, por ejemplo, una
aplicación de comercio electrónico que ha de contactar con un banco para comprobar la validez
de una tarjeta de crédito. No resulta demasiado difícil imaginar la impresión del usuario final
cuando la implementación utiliza hebras y cuando no lo hace.
Control de la ejecución de una hebra
Aún nos faltaba por hacer una cosa más con nuestra aplicación Windows. Una vez que el
usuario ordena la ejecución de una tarea computacionalmente costosa, sería deseable que
siempre mantuviese el control sobre lo que hace el ordenador (un principio básico en el diseño
de interfaces de usuario).
Para que el usuario mantenga el control absoluto sobre la ejecución de nuestra aplicación,
podemos hacer que el botón mediante el cual se inició el cálculo de PI sirva también para
detenerlo (o crear una ventana de diálogo independiente en la cual se incluya algún botón
con la misma funcionalidad). Si el usuario decide cancelar la operación en curso, la hebra
que controla la interfaz de usuario debe comunicarle a la otra hebra que detenga su
ejecución y, mientras ésta no se detenga, tenemos que deshabilitar el botón (con el fin de
evitar que, por error, se cree una nueva hebra en el intervalo de tiempo que abarca desde
que el usuario pulsa el botón de cancelar hasta que la hebra realmente se detiene).
Para que la hebra detenga su ejecución, podemos hacer que las hebras se
comuniquen a través de paso de parámetros (por ejemplo, haciendo que la función
ShowProgress devuelva un valor a través de un parámetro adicional, que será
comprobado en cada iteración de la hebra para determinar si el usuario ha pedido
cancelar la operación en curso). También podemos emplear datos compartidos por las
hebras, aunque, en ese caso, deberemos tener especial cuidado con los problemas de
sincronización y posibles bloqueos que puedan ocurrir. Si utilizamos esta última opción,
podemos escribir lo siguiente:
delegate void EndProgressDelegate ();
private void CalcularPi (int precision)
{
EndProgressDelegate endProgress = new
EndProgressDelegate(EndProgress);
...
while ( (estado!=Estado.Cancelando) && ...) {
...
}
this.Invoke(endProgress);
}
private void EndProgress ()
{
estado = Estado.Inactivo;
buttonCalc.Text = "Calcular";
buttonCalc.Enabled = true;
}
Igual que antes, para que el usuario mantenga el control absoluto sobre la ejecución
de nuestra aplicación, hacemos que el botón mediante el cual se inició el cálculo de
PI sirva también para detenerlo:
// Variable de estado
progress.Maximum = precision;
progress.Value = Math.Min(precision, 9*completed);
}
estado = Estado.Inactivo;
buttonCalc.Text = "Calcular";
buttonCalc.Enabled = true;
}
int hebras = 0;
delegado.BeginInvoke(null, null);
lock (this) {
hebras++;
}
}
if (hebras==0) {
EndProgressDelegate endProgress = new
EndProgressDelegate(EndProgress);
this.Invoke(endProgress);
}
}
private string Pi ()
{
StringBuilder pi = new StringBuilder("3.", precision + 2);
return pi.ToString();
}
lock (this) {
current++;
}
return segment;
}
Por último, ya sólo nos queda implementar el cuerpo de las hebras, que se
encargará de ir calculando el valor exacto de pi por segmentos:
segments[segment] = NineDigitsOfPi.StartingAt(9*segment+1);
endThread();
}
Referencias
Shawn Cicoria: Proper Threading in Winforms .NET. CodeProject, May 2003.
Chris Sells: Safe, Simple Multithreading in Windows Forms, Part 1. MSDN,
June 28, 2002.
Chris Sells: Safe, Simple Multithreading in Windows Forms, Part 2. MSDN,
September 2, 2002.
Chris Sells: Safe, Simple Multithreading in Windows Forms, Part 3. MSDN,
January 23, 2003.
David Carmona: Programming the Thread Pool in the .NET Framework. MSDN,
June 2002.
Sincronización
El código ejecutado por una hebra debe tener en cuenta la posible existencia de otras hebras
que se ejecuten concurrentemente. Hay que tener cuidado para evitar que dos hebras accedan
a un recurso compartido al mismo tiempo (vg: objeto o variable global). Además, la ejecución
de una hebra puede depender del resultado de las tareas que realicen otras hebras. En
cualquier caso, las distintas hebras de una aplicación han de coordinar su ejecución.
Nota
El modelo de sincronización utilizado en la plataforma .NET es
completamente análogo al utilizado por el lenguaje de programación
Java.
Para evitar conflictos con otras hebras, puede que se necesite bloquear la ejecución de otras
hebras al acceder a objetos o variables compartidas. En tal caso, hay que tener cuidado de no
bloquear innecesariamente la ejecución de otras hebras para no disminuir el rendimiento de la
aplicación.
Monitores
La clase System.Threading.Monitor proporciona un modelo de coordinación similar a las
secciones críticas en Win32 (Enter/TryEnter/Exit), que garantizan la exclusión mutua en
el acceso a recursos compartidos:
Sección crítica
Monitor.Enter(this);
try {
...
} finally {
Monitor.Exit(this);
}
La misma clase, System.Threading.Monitor, también proporciona un mecanismo de
coordinación similar a los semáforos (Wait/Pulse/PulseAll). En este caso, una hebra llama
al método Wait(obj) y queda a la espera de que otra hebra invoque al método Pulse(obj)
(o PulseAll(obj)) para proseguir su ejecución. Las sincronización se puede realizar
utilizando cualquier objeto gestionado por la plataforma .NET.
Cerrojos (locks)
La sentencia lock de C# nos permite implementar una sección crítica equivalente al uso de
Monitor.Enter y Monitor.Exit. Sólo una de las sentencias lock puede tener acceso a un
objeto concreto:
lock (this) {
El acceso a objetos compartidos cuyo estado puede variar ha de hacerse siempre de forma
protegida:
Cuando queremos proteger el acceso a un miembro estático de una clase, se utiliza el objeto
de tipo System.Type asociado a la clase que contiene el miembro estático al que queremos
acceder:
Lectores y escritores...
class RWLock
{
int lectores = 0;
Eventos
Mutex
Timer
Interlocked
Operaciones asíncronas
Generalmente, al llamar a un método, la ejecución de nuestra hebra queda bloqueada hasta
que el método finaliza su ejecución. Esto puede resultar particularmente inoportuno cuando
realizamos costosas operaciones de entrada/salida. De forma análoga a la ejecución asíncrona
de delegados, la plataforma .NET nos permite realizar determinadas operaciones de forma
asíncrona. Por ejemplo, nuestra aplicación no tiene por qué detenerse cuando realizamos
operaciones de E/S.
E/S asíncrona
1. Estructura de datos auxiliar:
public class StateObject
{
public byte[] bytes;
public int size;
public FileStream fs;
}
2. Inicio de la operación de lectura asíncrona:
...
StateObject state;
AsyncCallback callback;
FileStream fs;
state.fs = fs;
state.size = fs.Length;
state.bytes = new byte[state.size];
if (bytesRead != state.size)
throw new Exception
("Sólo se han leído {0} bytes", bytesRead);
stream.Close();
Información adicional
El portapapeles de Windows [Windows Clipboard], que, como almacén central de datos, nos
permite que distintas aplicaciones intercambien datos "copiando y pegando".
DDE [Dynamic Data Exchange], un protocolo que permite a las aplicaciones intercambiar
datos de una forma más general que el portapapeles.
OLE [Object Linking and Embedding], basado en COM, nos permite manipular documentos
compuestos de datos que pueden provenir de distintas aplicaciones.
ActiveX es otra tecnología basada en COM que permite la comunicación entre componentes
independientemente del lenguaje en el que estén implementados.
COM [Component Object Model] establece un estándar binario mediante el cual se puede
acceder a los servicios de un componente utilizando uno o varios conjuntos de funciones
relacionadas (interfaces).
DCOM [Distributed COM] extiende el modelo de programación COM para que distintos
componentes puedan comunicarse a través de una red.
.NET Remoting, el mecanismo que sustituye a DCOM en la plataforma .NET.
NetBIOS [Network Basic Input/Output System], un protocolo para redes de área local de
PCs creado por IBM en la época del MS-DOS que es similar a Novell Netware (IPX/SPX).
Sockets, para los cuales Windows incluye un API más sofisticado que el API tradicional que
proviene del BSD UNIX [Berkeley Software Distribution], el API que se ha utilizado
habitualmente para transmitir datos utilizando la familia de protocolos TCP/IP.
Pipes anónimos [Anonymous pipes]: Permiten redireccionar la entrada o salida estándar de
un proceso (utilizando | en la línea de comandos, por ejemplo).
Pipes con nombre [Named pipes], similares a las colas FIFO de POSIX [Portable Operating
System Interface for Computer Environments. IEEE 1003.1, 1988] pero no compatibles con
ellas, permiten a dos procesos intercambiar mensajes utilizando una sección de memoria
compartida [pipe].
Mailslots, en Win32 y OS/2: Proporcionan un mecanismo de comunicación entre procesos
unidireccional y no fiable que puede ser útil para difundir mensajes cortos a múltiples
procesos en un sistema distribuido.
WM_COPYDATA, un mensaje de Windows que se puede utilizar para transmitir datos de un
proceso a otro utilizando la infraestructura del propio sistema operativo.
Ficheros mapeados en memoria: Permiten que dos procesos de una misma máquina
compartan un fichero que pueden manipular como si fuese un bloque de memoria en su
espacio de direcciones, si bien para acceder a él correctamente deberán utilizar algún
mecanismo de exclusión mutua (vg: semáforos).
Semáforos, eventos, Mutex y otras primitivas de sincronización que permiten comunicar la
ocurrencia de alguna acción o de algún cambio de estado.
RPC [Remote Procedure Call]: Llamadas a procedimientos remotos. Permiten realizar la
comunicación entre procesos como si se tratase de llamadas a funciones. El RPC de
Windows cumple con el estándar OSF DCE [Open Software Foundation Distributed
Computing Environment], lo que permite la comunicación entre procesos que se ejecuten en
sistemas operativos diferentes a Windows.
CORBA [Common Object Request Broker Architecture], estándar del OMG [Object
Management Group] para el desarrollo de sistemas distribuidos.
MPI [Message Passing Interface]: Estándar de paso de mensajes muy utilizado en clusters y
supercomputadores.
PVM [Parallel Virtual Machine]: Otro estándar de paso de mensajes utilizado en
multiprocesadores y multicomputadores.
Servicios web [Web services]: Conjunto de estándares que facilitan el paso de mensajes en
entornos heterogéneos.
Además, lo ideal es que intentemos que en nuestra aplicación pueda modificar el mecanismo
de comunicación entre procesos utilizado con el menor esfuerzo posible, algo que no siempre
facilitan los estándares existentes.
En cualquier caso, lo que siempre debemos tener en cuenta a la hora de desarrollar sistemas
distribuidos es que los mecanismos de comunicación no siempre son fiables (algunos paquetes
se pierden), la comunicación entre procesos consume tiempo (la latencia no es cero), la
capacidad del canal de comunicación no es infinita (el ancho de banda es un recurso muy
valioso) y las comunicaciones no siempre se realizan a través de medios seguros.
Funcionamiento
La familia TCP/IP define protocolos para las capas de red, transporte y aplicación, pero no para
las capas de más bajo nivel en el modelo OSI, de forma que los protocolos TCP/IP pueden
funcionar sobre distintos tipos de redes de ordenadores:
El protocolo IP: La capa de red en Internet
Tanto TCP como UDP permiten multiplexar conexiones mediante "puertos", los cuales sirven de
punto de acceso a las distintas aplicaciones que se ejecutan en una máquina con una dirección
IP concreta. Algunos de estos puertos están asignados a los protocolos de las aplicaciones más
comunes de Internet:
TCP se encarga de realizar el control de flujo de extremo a extremo (esto es, garantizar la
entrega ordenada de los paquetes y proceder a su retransmisión cuando haga falta), mientras
que UDP no garantiza ni la entrega ni la no existencia de duplicados (a cambio de un overhead
reducido).
El estándar que define las primitivas de servicio ofrecidas por el protocolo TCP denomina
"sockets" [enchufes] a los extremos de las conexiones TCP, de ahí el nombre que se le da al
mecanismo de comunicación entre procesos que utiliza la familia de protocolos TCP/IP.
Uso de TCP
En la plataforma .NET, las clases que facilitan el uso de sockets se encuentran en los espacios
de nombres System.Net y System.Net.Sockets, por lo cual es recomendable incluir las
correspondientes sentencias using ...; al comienzo de los ficheros de código C# en los que
utilicemos dichas clases.
// Puerto
IPEndPoint localEndPoint = new IPEndPoint(ipAddress,
11000);
Una vez que tenemos el puerto a través del cual aceptaremos conexiones, creamos
físicamente el socket y lo configuramos para que pueda aceptar conexiones:
En este caso hemos utilizado los sockets de la forma tradicional (la misma que originalmente
se ideó en la distribución BSD de UNIX): el servidor se queda esperando al llamar a Accept
hasta que llegue alguna petición. En Windows, no obstante, también podríamos haber utilizado
sockets de forma asíncrona, para evitar que el servidor quede bloqueado indefinidamente.
Cuando la llamada a Accept devuelve un nuevo socket, entonces podemos realizar la tarea
para la cual hayamos diseñado nuestro servidor. Por ejemplo, podemos construir un servidor
que haga de eco, devolviéndole al cliente lo mismo que éste le envíe:
do
{
count = handler.Receive(bytes);
data += System.Text.Encoding.ASCII.GetString(bytes,0,count);
} while ( data.IndexOf("\n") == -1 );
// Eco
handler.Send(msg);
handler.Shutdown(SocketShutdown.Both);
handler.Close();
Código fuente del servidor TCP utilizando sockets (Visual Studio 2003)
Código fuente del servidor TCP utilizando sockets (Visual Studio 2005)
Para comprobar que nuestro servidor TCP funciona correctamente, podemos utilizar la utilidad
telnet para establecer una conexión con el puerto asociado a nuestro servidor:
La plataforma .NET incluye una clase auxiliar denominada TcpListener que simplifica algo la
creación de servidores TCP, si bien internamente se sigue haciendo lo mismo:
listener.Start();
...
La única diferencia reseñable es que ahora, en vez de utilizar Send y Receive, el objeto de
tipo TcpClient dispone de un stream a través del cual leemos y escribimos datos.
Código fuente del servidor TCP utilizando TcpListener (Visual Studio 2003)
Código fuente del servidor TCP utilizando TcpListener (Visual Studio 2005)
Escribir un cliente TCP es aún más sencillo. A continuación se muestra un ejemplo de cliente
TCP que se conecta al servidor implementado en la sección anterior:
// DNS
IPAddress address = Dns.Resolve("localhost").AddressList[0];
// EndPoint
IPEndPoint EPhost = new IPEndPoint(address, 11000);
// Socket
Socket socket = new Socket ( AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp );
// Conexión
try {
socket.Connect (EPhost);
socket.Send( SendBytes,
SendBytes.Length,
SocketFlags.None);
respuesta = Encoding.ASCII.GetString
(RecvBytes, 0, bytes);
respuesta += Encoding.ASCII.GetString
(RecvBytes, 0, bytes);
}
Console.WriteLine(respuesta);
Igual que antes, la plataforma .NET incluye otra clase auxiliar que nos puede servir de utilidad
para crear clientes TCP: la clase TcpClient.
// Cliente TCP
// Conexión
try {
client.Connect("localhost",11000);
stream = client.GetStream();
stream.Write ( SendBytes,
0,SendBytes.Length );
respuesta = Encoding.ASCII.GetString
(RecvBytes, 0, bytes);
respuesta += Encoding.ASCII.GetString
(RecvBytes, 0, bytes);
}
Console.WriteLine(respuesta);
Al implementar un servidor TCP real, tendremos que ser capaces de aceptar conexiones de
varios clientes simultáneamente, por lo que tendremos que crear una aplicación multihebra. La
forma más sencilla de hacerlo en .NET es mediante la ejecución asíncrona de delegados.
Así es como quedaría nuestro servidor TCP de eco en una implementación más realista:
public class EchoServer
{
[STAThread]
static void Main(string[] args)
{
// Dirección IP
IPHostEntry ipHostInfo = Dns.Resolve(Dns.GetHostName());
IPAddress ipAddress = ipHostInfo.AddressList[0];
// Puerto
IPEndPoint localEndPoint = new IPEndPoint(ipAddress, 11000);
// Listener
Socket listener = new Socket ( AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp );
listener.Bind(localEndPoint);
listener.Listen(100); // backlog
Console.WriteLine("SERVIDOR DE ECO");
Console.WriteLine("Esperando una conexión...");
// Socket asíncrono
listener.BeginAccept( new AsyncCallback(AcceptCallback),
listener );
// Socket.
public Socket workSocket = null;
// Cadena recibida
public StringBuilder sb = new StringBuilder();
}
Cada vez que aceptemos una conexión, crearemos un nuevo socket asíncrono
para atender nuevas peticiones y recibiremos datos de la conexión aceptada
(también de forma asíncrona):
listener.BeginAccept(new AsyncCallback(AcceptCallback),
listener);
if (bytesRead > 0) {
state.sb.Append(Encoding.ASCII.GetString(state.buffer, 0,
bytesRead));
content = state.sb.ToString();
Ya sólo nos queda implementar la parte del servidor que le devuelve al cliente lo
que éste le haya enviado. Una vez más, lo hacemos de forma asíncrona:
private static void Send(Socket handler, String data)
{
byte[] byteData = Encoding.ASCII.GetBytes(data);
handler.EndSend(ar);
handler.Shutdown(SocketShutdown.Both);
handler.Close();
} catch (Exception e) {
Console.WriteLine(e.ToString());
}
}
request.KeepAlive = false;
Console.WriteLine ("HEADERS");
Console.WriteLine (response.Headers.ToString());
// Contenido del recurso al que se accede
Stream stream;
StreamReader reader;
Encoding encoder = Encoding.GetEncoding("utf-8");
Console.WriteLine ("CONTENT");
stream = response.GetResponseStream();
reader.Close();
stream.Close();
response.Close();
Código fuente del cliente HTTP/HTTPS (Visual Studio 2003)
Código fuente del cliente HTTP/HTTPS (Visual Studio 2005)
NOTA: La plataforma .NET nos permite utilizar los mismos interfaces ( WebRequest y
WebResponse) para acceder al sistema de archivos local mediante las clases
FileWebRequest y FileWebResponse. En este caso, lo que hacemos es acceder a recursos
utilizando URLs de la forma file://....
Correo electrónico
Como no podía ser menos, la plataforma .NET también incluye un componente que nos
permite enviar correo electrónico utilizando el protocolo estándar de Internet para el envío de
e-mails: el protocolo SMTP [Simple Mail Transfer Protocol].
Podemos crear fácilmente un programa que nos permita enviar correos electrónicos utilizando
las clases del espacio de nombres System.Net.Mail si utilizamos Visual Studio 2005 (.NET
Framework 2.0) o bien las clases incluidas en System.Web.Mail si utilizamos Visual Studio
2003 (.NET Framework 1.1).
smtp.Send(from,to,subject,body);
Esta clase nos permite acceder a un servidor SMTP cualquiera (smtp.gmail.com en este
caso) para enviar correos electrónicos a través de él. Es probable que nuestro servidor
requiera autentificación, por lo que también tendremos que indicar nuestro nombre de usuario
y contraseña de la siguiente forma:
smtp.Credentials = new
System.Net.NetworkCredential("usuario","********");
smtp.EnableSsl = true;
smtp.Send(from,to,subject,body);
message.Subject = "...";
message.IsBodyHtml = true;
message.Body = "<html><body> ... </body></html>";
smtp.Send(message);
dialog.InitialDirectory = "c:\\" ;
dialog.Filter = "Ficheros de texto (*.txt)|*.txt" +
+ "|Cualquier fichero (*.*)|*.*" ;
dialog.FilterIndex = 2 ;
dialog.RestoreDirectory = true;
if (dialog.ShowDialog() == DialogResult.OK) {
message.Subject = "...";
message.IsBodyHtml = true;
message.Body = "<html><body>"+...+"</body></html>";
message.Attachments.Add(attachment);
smtp.Send(message);
}
Código fuente de una aplicación Windows para enviar correo electrónico (.NET Framework
2.0, Visual Studio 2005)
Envío de correo electrónico en Visual Studio 2003 (.NET Framework
1.1)
Si consultamos la documentación de este servidor simple SMTP, podremos ver que se limita a
almacenar los mensajes de correo dirigidos a dominios locales en un directorio local
(C:\inetpub\mailroot\...\*.eml) e intenta redireccionar los mensajes externos a algún
servidor SMTP adecuado, sin garantizar su entrega (el subdirectorio Drop almacena los
mensajes que nunca llegaron a su destino).
Nota
Para utilizar las clases del espacio de nombres System.Web.Mail
debemos asegurarnos de que nuestro proyecto en Visual Studio .NET
incluya una referencia a la DLL System.Web.dll.
La clase System.Web.Mail.MailMessage nos permite sacarle mayor partido al envío de
mensajes de correo electrónico. Por ejemplo, podemos enviar e-mails en formato HTML:
System.Web.Mail.MailMessage message;
System.Web.Mail.SmtpMail.Send(message);
dialog.InitialDirectory = "c:\\" ;
dialog.Filter = "Ficheros de texto (*.txt)|*.txt"
+"|Cualquier fichero (*.*)|*.*" ;
dialog.FilterIndex = 2 ;
dialog.RestoreDirectory = true ;
if (dialog.ShowDialog() == DialogResult.OK) {
System.Web.Mail.MailAttachment attachment =
new System.Web.Mail.MailAttachment(dialog.FileName);
System.Web.Mail.MailMessage message;
message.Attachments.Add(attachment);
System.Web.Mail.SmtpMail.Send(message);
}
Código fuente de una aplicación Windows para enviar correo electrónico (.NET Framework
1.1, Visual Studio 2003)
using System.Web.Mail
...
MailMessage message;
message.From = "fberzal@decsai.ugr.es";
message.Bcc = "jc.cubero@decsai.ugr.es";
message.Headers.Add ( "Reply-To",
"berzal@acm.org");
message.Headers.Add ( "From",
"Fernando Berzal <berzal@acm.org>");
message.Fields.Add (
"http://schemas.microsoft.com/cdo/configuration/smtpauthenticate",
"1" );
message.Fields.Add(
"http://schemas.microsoft.com/cdo/configuration/sendusername",
"fberzal" );
message.Fields.Add(
"http://schemas.microsoft.com/cdo/configuration/sendpassword",
"*********" );
SmtpMail.SmtpServer = "correo.ugr.es";
SmtpMail.Send( message );
Uso de UDP
UDP [User Datagram Protocol] es el protocolo que proporciona los servicios no orientados a
conexión en la familia TCP/IP. Esto implica que, cuando el cliente envía un mensaje, no existe
modo alguno en que pueda comprobar si el servidor llegó a recibirlo (algo así como el correo
convencional).
Por ejemplo, podemos utilizar UDP para construir una aplicación Windows que nos permita
chatear:
Aunque un sistema real sería más sofisticado, vamos a utilizar difusión de mensajes
[broadcasting] para hacer que el chat funcione en todos los ordenadores conectados a una
misma red puedan comunicarse. Para ello necesitamos la dirección de broadcast de nuestra
red. Esta dirección está formada por la dirección de la red y una secuencia binaria de unos. La
dirección de red es el resultado de hacer la operación lógica AND entre la dirección IP de
nuestra máquina y la máscara de red. Por ejemplo, si mi ordenador tiene la dirección IP
150.214.191.234 y la máscara de red es 255.255.255.0, la dirección IP de mi red es
150.214.191.0, que al completarla con unos queda como 150.214.191.255.
IPAddress broadcastAddress =
IPAddress.Parse("192.168.1.255");
IPAddress localAddress =
Dns.Resolve(Dns.GetHostName()).AddressList[0];
int Port = 11000;
Una vez que sabemos las direcciones IP utilizadas para enviar y recibir datagramas UDP y el
puerto a través del cual funcionará nuestra aplicación, tenemos que utilizar una hebra que se
encargue de ir recibiendo mensajes (que declararemos como miembro de nuestra clase
formulario):
// Arranque
// Detención
Ahora bien, la hebra que recibe mensajes no es la misma hebra que controla la interfaz de
usuario de nuestra aplicación, por lo que debemos asegurarnos de que nunca se accede a la
interfaz de usuario desde una hebra distinta a la hebra responsable de su gestión. Para ello,
podemos definir delegados, comprobar el valor de la propiedad InvokeRequired y recurrir al
método Invoke del formulario:
// Hebra correcta
textBoxDialog.AppendText(text);
} else {
Ya tenemos listo todo lo necesario para poder centrarnos en el envío y recepción de mensajes
del chat. Del envío de mensajes se encargará la función Send, cuya implementación es
bastante simple:
try {
s.SendTo ( msg,
0, msg.Length,
SocketFlags.None,
ep );
} catch (Exception e) {
MessageBox.Show ( e.ToString(),
"Error enviando datos");
}
}
Esta función será invocada cada vez que el usuario introduzca un retorno de carro en el
TextBox dedicado a la introducción de mensajes:
Finalmente, sólo nos queda por implementar el cuerpo de la hebra que se encarga de ir
recibiendo datagramas UDP y nos permite ver lo que los demás usuarios del chat escriben:
try {
socket.Bind(localEP);
while (!done) {
if (!done) {
AppendText(mensaje);
}
}
} catch (Exception e) {
MessageBox.Show ( e.ToString(),
"Error recibiendo datos" );
}
}
Código fuente del chat UDP (Visual Studio 2003)
Código fuente del chat UDP (Visual Studio 2005)
NOTA: Igual que sucedía con TCP, podríamos haber utilizado una clase específica
proporcionada por la plataforma .NET para ahorrarnos un algunas líneas de código. En este
caso, la clase se llama UdpClient. Esta clase sirve igualmente de servidor y de cliente, ya
que en UDP no hay servidores y clientes: todo el mundo envía y todo el mundo escucha.
.NET Remoting
Un poco de historia
En el desarrollo de sistemas complejos usando técnicas de orientación a objetos, una interfaz
simple a nivel de bytes como la de los sockets no resulta del todo apropiada. Por eso existen
distintas tecnologías mediante las cuales un objeto puede exponer sus interfaces al público
para facilitar su utilización a un nivel de abstracción mayor.
En el modelo COM [Component Object Model], todos los objetos han de implementar la
interfaz IUnknown mediante la cual se controla su ciclo de vida (contando las referencias
existentes a un objeto se puede saber cuándo puede ser éste eliminado) y se pueden explorar
sus características (consultando los interfaces que implementa o, más bien, preguntando si el
objeto soporta un interfaz dado):
[uuid(00000000-0000-0000-C000-000000000046)]
interface IUnknown {
HRESULT QueryInterface (
[in] const IID iid,
[out, iid_is(iid)] IUnknown iid );
Hay que mencionar que COM es un estándar binario que ni requiere ni impide el uso de
orientación a objetos en el diseño de objetos COM. De hecho, COM se limita a la especificación
de interfaces y no permite herencia de implementación (lo que puede considerarse algo
positivo en el desarrollo de componentes software, donde es preferible usar composición en
vez de herencia), si bien sí permite herencia simple de interfaces (aunque también es cierto
que esta característica tampoco se usa mucho, ya que se prefieren definir categorías que no
son más que conjuntos de interfaces).
Una vez publicado una intrefaz con su IID, su especificación no puede modificarse bajo
ninguna circunstancia (un componente puede implementar distintas versiones de un interfaz
pero éstas se tratan en realidad como interfaces diferentes implementados por el componente,
lo que, por otro lado, evita los conflictos de nombres que se producirían en un modelo
orientado a objetos convencional).
DCOM [Distributed COM] se limita a ampliar el modelo COM de forma transparente mediante la
utilización interna de un mecanismo de comunicación entre procesos basado en el uso de
llamadas a procedimientos remotos usando un estándar binario definido por DCOM.
COM+ es otra extensión de COM que apareció en el año 2000 con Windows 2000 Server. Su
primera versión, COM+ 1.0, integra COM con distintas tecnologías, tales como el
procesamiento de transacciones (con MTS [Microsoft Transaction Server]) o el envío de
mensajes asíncronos (mediante MSMQ [Microsoft Message Queue server]), entre otras. Su
segunda versión, COM+ 2.0, es la plataforma .NET.
La principal innovación que supone COM+ (y, por ende, la plataforma .NET) en la evolución de
los productos de Microsoft es la introducción de atributos declarativos. Estos atributos
permiten separar distintos aspectos en el mismo sentido en que el desarrollo de software
orientado a aspectos permite identificar y aislar asuntos compartidos por distintos
componentes. Para entender esto de forma intuitiva, digamos que los aspectos sirven para
centralizar código que de otra forma aparecería duplicado y esparcido por distintas partes de
una aplicación (p.ej. control de acceso, serialización, sincronización, transacciones...).
Dominios de aplicación
El CLR [Common Language Runtime] de la plataforma .NET divide cada proceso en uno o
varios dominios de aplicación. Dichos dominios aislan los objetos que contienen de todos los
demás que queden fuera del dominio, de forma que para que dos objetos de distintos dominios
se puedan comunicar es necesario utilizar "marshalling" (el mecanismo mediante el cual se
empaquetan datos para su transmisión, también conocido como serialización):
El siguiente fragmento de código muestra el nombre del dominio de aplicación actual y de los
assemblies que se hallan en él:
using System.Reflection;
using System.Runtime.Remoting;
...
Los dominios de aplicación constituyen unidades aisladas en el CLR. En ellos, una aplicación
puede ejecutarse o detenerse sin afectar a las aplicaciones que se ejecutan en otros dominios
de aplicación. De hecho, una aplicación no puede acceder directamente a los recursos que se
encuentren en un dominio de aplicación distinto del suyo. Gracias a ello, un fallo en una
aplicación se puede mantener confinado en los límites de un dominio de aplicación de forma
que, aunque distintos dominios de aplicación estén en un mismo proceso, los demás dominios
de aplicación no se verán afectados por el fallo.
Contextos
Los contextos definen una partición de los dominios de aplicación, de tal forma que los objetos
pertenecientes a un contexto comparten las propiedades de su contexto. Los contextos en
COM+ derivan de los apartamentos COM (el mecanismo mediante el cual se controla la
sincronización entre hebras en COM) y de los contextos MTS (que separan objetos en función
de su "dominio transaccional").
Un objeto puede ser ágil [context-agile] o estar ligado a un contexto [context-bound], lo cual
viene predeterminado si el objeto deriva de la clase System.ContextBoundObject. Un
objeto ágil A puede interactuar con un objeto B ligado a un contexto como si A estuviese en el
mismo contexto que B. Esto es, puede llamarse a A desde cualquier contexto o dominio de
aplicación libremente. Cualquier llamada que requiera pasar los límites de un contexto es
interceptada y, dependiendo de las propiedades del contexto, es preprocesada, postprocesada
o, simplemente, rechazada.
Arquitectura de .NET Remoting
Sobre la infraestructura definida por los contextos y empleando la capacidad de reflexión del
CLI [Common Language Infraestructure], .NET Remoting (=CLR Object Remoting) proporciona
las bases para construir una amplia variedad de estilos de comunicación: desde llamadas
síncronas (como DCOM) hasta llamadas completamente asíncronas (con la posibilidad de
sondeo y notificación de la terminación de la llamada), ya sea utilizando una codificación
binaria sobre un canal TCP o empleando SOAP (en XML y, usualmente, sobre HTTP).
Canales
Formateadores
Los formateadores se encargan de serializar los objetos .NET para que puedan transmitirse a
través de los canales de comunicación. La plataforma .NET puede serializar los objetos en
binario y en SOAP/XML. El formateador binario es más eficiente, el que emplea XML resulta
más cómodo a la hora de integrar sistemas heterogéneos. Además, podemos crear
formateadores específicos que se adapten a nuestras necesidades.
La diferencia clave entre .NET Remoting y los Servicios Web se encuentra en la forma en que
serializan los datos. Los Servicios Web emplean siempre XML, .NET Remoting puede emplear
cualquier formateador que implemente la interfaz
System.Runtime.Remoting.Messaging.IRemotingFormatter, como
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter o
System.Runtime.Serialization.Formatters.Soap.SoapFormatter.
Por defecto, los canales HTTP utilizan el formateador SOAP, mientras que los canales TCP usan
el formateador binario para acceder a objetos remotos, si bien todo es configurable en .NET
Remoting. Cuando las llamadas remotas sólo cruzan los límites de un contexto pero se realizan
dentro de un dominio de aplicación, se utiliza un canal especial CrossContextChannel que
está optimizado para trabajar dentro del espacio de memoria del proceso y no requiere
formateador alguno.
Marshalling
Por valor (MarshalByValue), haciendo una copia del objeto completo, que se
transmite a través del canal perdiendo el enlace entre el original y la copia. El cliente
dispondrá localmente de una copia completa del objeto remoto. Esta copia, obviamente,
trabajará de forma independiente con respecto a la copia remota del objeto. En otras
palabras, en el momento en el que se accede a un objeto remoto por valor, el objeto deja
de ser remoto.
Por referencia (MarshalByRef), pasando únicamente una referencia al objeto
[ObjRef] y creando un "proxy" que sirve de enlace entre el cliente y el objeto remoto. Los
objetos remotos siempre residen y se ejecutan en el servidor. El cliente se comunica con
el objeto remoto a través del proxy, que sólo tiene una referencia al objeto remoto.
Los objetos ágiles (independientes del contexto) se transmiten por valor si no están ligados a
un dominio de aplicación, mientras que se transmiten por referencia si están asociados a un
dominio de aplicación y se accede a ellos desde fuera de ese dominio. Los objetos ligados a un
contexto siempre se transmiten por referencia fuera de ese contexto.
El acceso a un objeto por valor siempre será más rápido una vez que se dispone de una copia
local del objeto, si bien el tiempo que se tarda en obtener inicialmente esa copia puede ser
considerable si el objeto es grande. En realidad, lo único que se hace al acceder por valor a un
objeto remoto es "descargar" el objeto del servidor y trabajar con él localmente. Por el
contrario, al acceder por referencia a un objeto remoto, nuestra aplicación es realmente una
aplicación distribuida. Esto puede ser útil cuando los objetos remotos son demasiado grandes
(lo que hace prohibitiva su transmisión hasta el cliente) o cuando los objetos remotos residen
en un servidor desde el cual se puede acceder a recursos no disponibles directamente desde el
cliente.
Proxy
Cuando un cliente quiere acceder remotamente a un objeto del servidor, .NET Remoting crea
automáticamente un proxy transparente que hace de servidor en el lado del cliente, de forma
que el cliente trabaja con él como si del propio objeto remoto se tratase. El proxy implementa
todos los métodos que aparecen en la interfaz del objeto remoto. Las llamadas que recibe las
envía al objeto remoto, que es el que verdaderamente se encarga de hacer el trabajo.
Dispatcher
El "dispatcher" se sitúa al otro extremo del canal, recibe los mensajes del proxy, invoca al
método real en el objeto remoto, recoge la el resultado y devuelve un mensaje de respuesta.
Uso de .NET Remoting
El modelo de activación de objetos de la plataforma .NET se parece más al de CORBA que al de
COM:
Nota
En los proyectos que utilizan clases del espacio de nombres
System.Runtime.Remoting es necesario agregar una referencia a la
DLL System.Runtime.Remoting.dll.
using System;
namespace RemotingExample
{
public class Servidor
{
[STAThread]
static void Main(string[] args)
{
Console.WriteLine ("Servidor cargado y ejecutado");
}
Ahora intentamos usarlo desde un cliente que creamos como aplicación independiente
(cliente.exe). Para que esta aplicación funcione correctamente, añadiremos una referencia
al proyecto servidor.exe (algo que podemos hacer directamente desde el explorador de
soluciones del Visual Studio).
using System;
using System.Reflection;
using System.Runtime.Remoting;
namespace RemotingExample
{
public class Cliente
{
[STAThread]
static void Main(string[] args)
{
AppDomain newDomain;
ObjectHandle o;
Servidor s;
// Nuevo dominio
AnewDomain = AppDomain.CreateDomain("MiNuevoDominio");
newDomain.ExecuteAssembly("Servidor.exe", null, args);
// Acceso al servidor...
o = newDomain.CreateInstance ( "Servidor",
"RemotingExample.Servidor");
// Fin
Console.WriteLine("Pulse ENTER...");
Console.ReadLine();
}
}
}
Al ejecutar el programa anterior, salta una excepción porque el servidor no es un objeto al que
se pueda acceder desde otro dominio de aplicación.
Para que el servidor sea accesible desde otro dominio de aplicación por valor, basta con
marcarlo como serializable. Como para acceder al objeto por valor hay que construir
localmente una copia exacta del objeto remoto, el objeto completo ha de transmitirse a través
del canal existente entre dominios de aplicación diferentes, para lo cual es imprescindible que
el objeto sea serializable.
using System;
namespace RemotingExample
{
[Serializable]
public class Servidor
{
[STAThread]
static void Main(string[] args)
{
Console.WriteLine ("Servidor cargado y ejecutado");
}
Hacer que al objeto remoto se pueda acceder por referencia es casi tan simple. Basta con
hacer que la clase Servidor derive de System.MarshalByRefObject.
using System;
namespace RemotingExample
{
public class Servidor: System.MarshalByRefObject
{
[STAThread]
static void Main(string[] args)
{
Console.WriteLine ("Servidor cargado y ejecutado");
}
Una vez que ya tenemos los conocimientos básicos necesarios para crear un servidor real al
que se acceda remotamente, creamos una biblioteca de clases ( Servidor.dll) en la que
incluimos la funcionalidad que nuestro servidor proporcionará a sus clientes:
El servidor lo tenemos que alojar en un dominio de aplicación, para lo cual creamos una
aplicación en modo de consola:
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;
using System.Runtime.Remoting.Channels.Tcp;
...
class Host
{
static void Main(string[] args)
{
ChannelServices.RegisterChannel(
new HttpChannel(8888));
RemotingConfiguration.RegisterWellKnownServiceType(
typeof(RemotingExample.Servidor),
"Servidor",
WellKnownObjectMode.SingleCall);
Console.WriteLine(
"Servidor listo para aceptar mensajes...");
Console.WriteLine(
"Pulse INTRO para salir");
Console.ReadLine();
}
}
Esta aplicación se encarga de crear y registrar un canal, ya que .NET Remoting exige que, para
que los clientes puedan acceder a los objetos remotos, éstos estén ligados a un canal cuyo
nombre sea conocido por el cliente. Por tanto, lo primero que hacemos es registrar un canal a
través del cual se pueda acceder al objeto de forma remota.
Acto seguido, se registran los tipos de objetos a los que se podrá acceder desde fuera del
dominio de aplicación del servidor, así como su URL de acceso. Los tipos registrados serán
accesibles desde el exterior mientras la aplicación que los aloja esté ejecutándose.
También hay que indicar el tipo de activación de los objetos a los que se accede de forma
remota. Desde el servidor, sólo se permiten dos tipos de activación en .NET Remoting:
SingleCall: Se crea una instancia de la clase para cada llamada realizada a través
del canal (comunicación sin estado, como en el protocolo HTTP).
Singleton: Existe una única instancia de la clase común para todos los clientes.
Este singleton sirve de gateway a la lógica de la aplicación.
Para poder hacer referencia al servidor desde el cliente, obtenemos una referencia al proxy del
objeto remoto mediante una llamada al método GetObject de la clase
System.Runtime.Remoting.Activator. Una vez que tenemos el proxy, podemos usar el
servidor como cualquier otro objeto:
// Acceso remoto
ChannelServices.RegisterChannel(new HttpChannel());
Console.WriteLine( remoto.GetInfo() );
// Acceso local
Console.WriteLine( local.GetInfo() );
El cliente, como es lógico, debe especificar el canal a través del cual se comunicará con el
objeto remoto. Este objeto podría incluso ofrecer sus servicios a través de distintos canales,
presumiblemente de distintos tipos. El cliente usará el canal que resulte más adecuado para
comunicarse con el objeto remoto.
Los metadatos de la clase Servidor necesarios para poder compilar el cliente los podemos
obtener del propio assembly (Servidor.dll) o de la referencia web
http://localhost:8888/Servidor?wsdl. A partir de esa referencia se puede crear el
ensamblado requerido por el cliente sin tener que distribuir el código completo del servidor.
Para lograrlo, podemos usar la utilidad soapsuds:
soapsuds -url:http://localhost:8888/Servidor?wsdl
-oa:InterfazServidor.dll
Cuando un objeto se registra en un canal para que se pueda acceder a él de forma remota, los
clientes podrán acceder a todas las propiedades, métodos y variables públicas no estáticas de
la clase a la que corresponda el objeto.
En el ejemplo anterior, el ciclo de vida del objeto remoto se controla en el servidor. Cuando un
cliente quiere acceder al objeto remoto, el cliente obtiene un proxy y en el servidor se crea una
instancia de la clase del objeto remoto para atender única y exclusivamente a una petición del
cliente. El objeto remoto sólo se instancia ("activa", si usamos la terminología de .NET
Remoting) cuando el cliente llama a un método del proxy. Esto nos permite ahorrar una
llamada inicial a través de la red para, simplemente, crear el objeto.
Si usamos el modo de activación SingleCall, el objeto remoto se instancia para atender una
única petición, tras la cual el objeto se elimina. Por tanto, esos objetos no mantenienen su
estado entre distintas peticiones provenientes de un mismo cliente. Esto, que a primera vista
es una seria limitación, permite construir aplicaciones distribuidas escalables, ya que el objeto
sólo consume recursos del servidor temporalmente y, en el caso de usar un cluster en el
servidor, la carga se puede distribuir con mayor facilidad (al no importar qué servidor atiende
cada petición porque éstas son independientes unas de otras). En definitiva, este modo de
activación es útil en aplicaciones que sólo requieren realizar una operación rápida e
independiente del resto de la aplicación como puede ser consultar un dato concreto en una
base de datos para mostrarlo en la interfaz de usuario.
Por el contrario, en el modo de activación Singleton, sólo tendremos una instancia del objeto
que atenderá todas las peticiones que se reciban de distintos clientes. A diferencia de
SingleCall, como el objeto existe de forma permanente, puede mantener el estado entre
distintas llamadas (estado que será global para todos los clientes que accedan a él). Por
ejemplo, podríamos usar este modo de activación para implementar correctamente un servidor
de chat como un objeto remoto único (en vez de usar direcciones de broadcast).
Un objeto singleton es un objeto único en su tipo que proporciona un punto de acceso global a
los servicios implementados por el objeto remoto. Cuando se hace una llamada al objeto
remoto, sólo se creará una instancia del objeto si previamente no existe, algo difícil de lograr
con DCOM, en el que los objetos remotos siempre se crean desde el cliente y hay que idear
mecanismos artificiales que nos permitan comprobar si existe ya un objeto de ese tipo para no
crear otro. En el caso de .NET Remoting, se simplifica la creación de "singletons", si bien su
implementación seguirá siendo compleja. Los objetos remotos de este tipo tendremos que
implementarlos como aplicaciones multihebra para que puedan atender concurrentemente las
peticiones de múltiples clientes.
Este tipo de objetos remotos, SAO (objetos activados por el servidor), proporcionan una
funcionalidad limitada porque sólo se pueden instanciar usando constructores por defecto, sin
parámetros. Pero no constituyen la única opción que tenemos a nuestra disposición en .NET
Remoting, como veremos a continuación
Ejercicio
Comprobar cuándo se crean y se destruyen los objetos en el servidor
cuando utilizamos los modos de activación Singleton y SingleCall
En situaciones como las vistas hasta ahora, el servidor controla cuándo se crea un objeto al
que se pueda acceder de forma remota. Como no podría ser menos, dada su flexibilidad, .NET
Remoting también permite que el cliente sea el que controle el ciclo de vida de los objetos a
los que accede de forma remota, al más puro estilo de COM/DCOM.
Este tipo de objetos, denominados CAOs [Client activated objects], se instancian en el servidor
cuando el cliente lo solicita (no se espera a que se llame a un método, como sucedía con los
SAOs). Esto permite utilizar constructores con parámetros si hace falta. La
instanciación/activación de un objeto CAO se realiza de la siguiente forma:
El cliente crea una instancia del objeto en el servidor mediante una solicitud de
activación.
El servidor crea una instancia de la clase (previamente registrada) y devuelve una
referencia al objeto recién creado.
El cliente utiliza esa referencia para crear un proxy mediante el cual pueda
comunicarse con el objeto remoto.
Como consecuencia del proceso seguido, la instancia del objeto remoto atenderá únicamente
las peticiones provenientes del cliente que creó el objeto (a diferencia de los SAOs Singleton,
que son objetos compartidos entre todos los clientes). Por ejemplo, se usaría un objeto CAO
cuando un cliente quiere realizar un pedido y la realización del pedido involucra ir pasando por
una serie de etapas, para las cuales ha de mantenerse en todo momento el estado del pedido.
La implementación en sí de un objeto CAO no difiere de la de un objeto SAO. Tendremos que
crear una clase que herede de System.MarshalByRefObject. Lo que sí tendremos que
cambiar es la aplicación que aloja al objeto:
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;
using System.Runtime.Remoting.Channels.Tcp;
...
class Host
{
static void Main(string[] args)
{
ChannelServices.RegisterChannel(new HttpChannel(8888));
RemotingConfiguration.ApplicationName = "Servidor";
RemotingConfiguration.RegisterActivatedServiceType (
typeof(RemotingExample.Servidor) );
Console.WriteLine(
"Servidor listo para aceptar mensajes...");
Console.WriteLine(
"Pulse INTRO para salir");
Console.ReadLine();
}
}
El cliente, como antes, deberá usar un canal HTTP para acceder al servidor. En esta ocasión no
obstante, se ha de utilizar la llamada al método estático CreateInstance de la clase
Activator. Este método devuelve una referencia a partir de la cual se puede obtener un
proxy con el método Unwrap y dicho proxy lo usaremos para acceder al objeto de forma
remota:
// Acceso remoto
ChannelServices.RegisterChannel(new HttpChannel());
object[] attrs =
{ new UrlAttribute("http://localhost:8888/Servidor")};
Console.WriteLine( remoto.GetInfo() );
En resumen, los CAOs ofrecen la máxima flexibilidad, mientras que los SAOs proporcionan una
mayor escalabilidad.
"Se alquila"
En .NET Remoting, el ciclo de vida de los objetos remotos se controla mediante leasing, la
realización de "contratos de alquiler", igual que en RMI o Jini en Java.
El lease determina el periodo de tiempo que el objeto estará activo en memoria antes de que
el CLR lo elimine. En el caso de los objetos activados por el servidor de tipo SingleCall,
estos sólo existen mientras dure una llamada a un método. En cambio, los SAOs de tipo
Singleton y los CAOs vivirán en función de sus "contratos de alquiler".
// En el objeto remoto
if (lease.CurrentState == LeaseState.Initial) {
lease.InitialLeaseTime = TimeSpan.FromMinutes(1);
lease.SponsorshipTimeout = TimeSpan.FromMinutes(2);
lease.RenewOnCallTime = TimeSpan.FromSeconds(30);
}
return lease;
}
...
}
Cada dominio de aplicación contiene un "gestor de alquileres" [lease manager], una hebra
más, que elimina los objetos cuando sus "contratos de alquiler" expiran. El gestor de alquileres
examina periódicamente los contratos para ver cuáles han caducado y eliminar los objetos
correspondientes (cada 10 segundos).
Cada vez que un "patrocinador" del objeto renueva su contrato de alquiler, dicho contrato se
amplía hasta el tiempo establecido por la propiedad RenewOnCallTime (120 segundos por
defecto), siempre y cuando fuese a caducar antes de ese plazo. Cuando el contrato caduca, se
espera el tiempo fijado por SponsorshipTimeout antes de eliminar físicamente el objeto, por
si algún patrocinador desea mantener el objeto (de nuevo, 120 segundos por defecto).
El patrocinador del objeto puede ser el cliente que accede a él o cualquier otro objeto
interesado en mantener al objeto remoto. Para renovar el contrato, el patrocinador debe
llamar al método Renew() del contrato asociado al objeto remoto, el cual se obtiene a través
de una llamada al método GetLifetimeService(). El patrocinador, además, recibirá una
notificación cada vez que el lease vaya a caducar, para lo cual ha de implementar la interfaz
ISponsor y registrarse como patrocinador del objeto:
remoto.GetLifeTimeService().Register ( patrocinador );
No obstante, el contrato se renueva automáticamente cada vez que se accede al objeto, por lo
que usualmente no tendremos que preocuparnos por este asunto. Si el contrato llegase a
caducar y el objeto remoto fuese eliminado, la llamada al método remoto generaría una
excepción (en el caso de los objetos CAO) o volvería a instanciar otro objeto (en el caso de los
objetos SAO Singleton, que son únicos en cualquier momento pero no siempre son los
mismos).
Ficheros de configuración
En los ejemplos vistos hasta ahora, el cliente debe conocer el tipo concreto del objeto remoto
que activa. Eso implica que cualquier cambio en la configuración del servidor requiere
recompilar todos los clientes.
En el servidor, una vez que tengamos el fichero de configuración, podemos registrar los
canales oportunos y establecer los objetos a los que se puede acceder de forma remota con
una simple llamada al método Configure de la clase RemotingConfiguration:
RemotingConfiguration.Configure ("Servidor.config");
donde Servidor.config es el fichero XML con todos los datos necesarios para la
configuración del servidor.
RemotingConfiguration.Configure ("Cliente.config");
Como ejemplo del uso de ficheros de configuración en .NET Remoting, crearemos un sencillo
servidor al que se accederá por referencia:
using System;
using System.Net;
using System.Runtime.Remoting;
namespace RemotingExample {
~Servidor() {
Console.WriteLine("Destructor");
}
using System;
using System.Runtime.Remoting;
namespace RemotingExample
{
class Host {
RemotingConfiguration.Configure("Servidor.config");
Console.WriteLine(
"Servidor listo para aceptar mensajes...");
Console.WriteLine(
"Pulse INTRO para salir");
Console.ReadLine();
}
}
}
El fichero de configuración Servidor.config especifica el canal que se utilizará para acceder
al servidor (un canal TCP en el puerto 7777) y el modo de activación del servidor
(SingleCall):
Servidor.config
<service>
<wellknown mode="SingleCall"
type="RemotingExample.Servidor, Servidor"
objectUri="RemotingExample.rem" />
</service>
<channels>
<channel port="7777" ref="tcp" />
</channels>
</application>
</system.runtime.remoting>
</configuration>
Cliente.config
<client>
<wellknown type="RemotingExample.Servidor, Servidor"
url="tcp://localhost:7777/RemotingExample/RemotingExample.rem" />
</client>
<channels>
<channel ref="tcp" />
</channels>
</application>
</system.runtime.remoting>
</configuration>
A partir de este fichero, el acceso remoto al servidor desde el cliente resulta trivial:
using System;
using System.Runtime.Remoting;
namespace RemotingExample
{
class Cliente
{
[STAThread]
static void Main(string[] args)
{
// Acceso remoto
RemotingConfiguration.Configure( "Cliente.config" );
Console.WriteLine( remoto.GetAppDomain() );
Console.WriteLine( remoto.GetApplication() );
Console.WriteLine( remoto.GetHost() );
Console.ReadLine();
}
}
}
Código fuente del ejemplo (Visual Studio 2003)
Código fuente del ejemplo (Visual Studio 2005)
NOTA: Para asegurarnos de que los ficheros de configuración se distribuyen junto con nuestros
ejecutables, tenemos que comprobar que la propiedad "Copiar en el directorio de resultados"
de nuestros ficheros de configuración .config esté activada.
Usando ficheros de configuración en XML, si nuestros objetos son de tipo SAO y utilizamos
canales HTTP, podemos utilizar el Internet Information Server para alojar nuestros objetos
remotos, sin necesidad de crear una aplicación que haga de servidor. Si alojamos la aplicación
en un directorio virtual llamado MiServicio dentro del IIS, se podrá acceder al objeto remoto a
través de la siguiente URL:
http://localhost/MiServicio/Servidor.soap
Servicios web
Arquitectura
Un servicio web es un componente software accesible a través de protocolos estándares de
Internet. Los servicios web se hallan en el núcleo de la plataforma .NET y de la visión del
software como servicio (en contraposición a la visión tradicional del software como producto).
Básicamente, los servicios web utilizan XML para facilitar que las aplicaciones distribuidas
puedan intercambiar datos en un formato sencillo fácilmente interpretable. Esto resulta de
particular interés en sistemas heterogéneos. De hecho, es en sistemas de este tipo donde
otras alternativas a los servicios web nunca han tenido demasiado éxito (DCOM está limitado
en la práctica a las plataformas de Microsoft, RMI sólo se usa en Java e incluso con CORBA se
producen problemas de interoperabilidad).
Los servicios web están diseñados como un mecanismo de paso de mensajes adecuada para
construir sistemas asíncronos débilmente acoplados. Debido al formato que utilizan para
transmitir mensajes (XML), los servicios web están pensados para utilizarse eficientemente en
la transmisión de documentos y no se deben interpretar como un simple mecanismo de
llamadas a procedimientos remotos (RPC), ya que para este fin resultan algo ineficientes.
Las aplicaciones basadas en servicios web se construyen empleando estándares como HTTP,
XML, SOAP, WSDL y UDDI. Estos tres últimos se utilizan para construir, describir y encontrar
servicios web, respectivamente.
SOAP es el protocolo mediante el cual se envían mensajes en formato XML. Como protocolo de
transporte, SOAP suele emplear HTTP, si bien se puede utilizar sobre cualquier otro protocolo
(SMTP, TCP, UDP...). Cuando se utiliza sobre HTTP, los mensajes SOAP se transmiten
mediante solicitudes HTTP POST en las que se ha de incluir una cabecera SOAPAction:
HTTP/1.1 POST /soap/myservice
Content-Type: text/xml
SOAPAction: MyInterface#MyComponentMethod
<SOAP:Envelope>
<SOAP:Header>
<MyHeader SOAP:mustUnderstand="0"> ... </MyHeader>
</SOAP:Header>
<SOAP:Body>
<MyRequest>
<argument>PI</argument>
</MyRequest>
</SOAP:Body>
</SOAP:Envelope>
HTTP/1.1 200 OK
...
Content-Type:text/xml
Content-Length: XXX
<?xml version="1.0"?>
<soap:Envelope ...>
<soap:Body>
<MyRequestResult>
<result>3.1416</result>
</MyRequestResult>
</soap:Body>
</soap:Envelope>
SOAP incluye un tipo de mensaje especial, denominado SOAP fault, que sirve para comunicar
distintos tipos de error que se pueden producir en el paso de mensajes.
<SOAP:Envelope>
<SOAP:Body>
<SOAP:Fault>
<faultcode>Server.InvalidArg</faultcode>
<faultstring>Type is wrong </faultstring>
<detail/>
</SOAP:Fault>
</SOAP:Body>
</SOAP:Envelope>
WSDL [Web Services Description Language]
WSDL se emplea para describir servicios web (de forma análoga a como los esquemas XML
describen documentos XML). Como no podía ser menos, las especificaciones WSDL son
documentos XML. Aunque su formato puede resultar algo complejo a primera vista, las
especificaciones WSDL resultan relativamente fáciles de interpretar en cuanto nos
acostumbramos a ellas.
<definitions name="serviceName">
<import namespace="http://namespacePath"
location="http://path/fileName.wsdl">
<portType name="serviceNamePortType">
<operation name="opName">
<input message="msgNameInput" />
<output message="msgNameOutput" />
</operation>
</portType>
<binding name="serviceNameSoapBinding">
<soap:operation soapAction="http://..." />
</binding>
<service name="serviceName">
<port name="serviceNamePort" binding="bindingName">
<soap:address location="http://..." />
</port>
</service>
</definitions>
En primer lugar, una especificación WSDL incluye algunas secciones con información abstracta
acerca de los mensajes y operaciones asociados al servicio web que describe. Un mensaje es lo
que se envía físicamente de un proceso a otro, mientras que una operación es una
combinación de mensajes y representa un grupo completo de mensajes relacionados. La
especificación WSDL también incluye una sección en la que se definen los tipos utilizados en
los mensajes (utilizando esquemas XML) y otra en la que se definen tipos de puertos
(definiciones abstractas de servicios web como grupos de operaciones).
Separadas de las secciones descritas en el párrafo anterior, las especificaciones WSDL incluyen
dos secciones en las que se recoge toda la información necesaria para poder utilizar un servicio
web. La sección <binding> especifica los protocolos utilizados en la transmisión de mensajes,
mientras que la sección <services> recoge los servicios ofertados. Cada servicio es, en
realidad, un conjunto de puertos o puntos de acceso a los servicios web.
DISCO
UDDI viene a ser como las páginas amarillas de los servicios web. El estándar UDDI, propuesto
por Microsoft, IBM y Ariba, define el formato de las entradas del directorio así como una serie
de APIs para
poder realizar consultas (descubrir servicios, ya sea realizando búsquedas al estilo de
los sistemas de recuperación de información o explorando categorías),
publicar servicios web (esto es, enviar su especificación para que sean registrados), y
replicar el contenido del directorio UDDI para formar una base de datos federada a
nivel global.
Tutorial: Creación de servicios web ASP.NET
Creación manual de un servicio web en IIS
\Windows\Microsoft.NET\Framework\v...\aspnet_regiis -i
using System;
using System.Web.Services;
Como se puede apreciar, lo único que tenemos que hacer es utilizar el atributo [WebMethod]
del espacio de nombres System.Web.Services para que nuestro método se convierta en un
servicio web.
Consideraciones de diseño
Aunque los entornos de programación como el Visual Studio .NET
generen automáticamente la interfaz del servicio web a partir de las
definiciones de clases, el diseño académicamente correcto de servicios
web requiere que primero diseñemos la interfaz del servicio web y
después creemos la implementación adecuada para el servicio
especificado. En otras palabras: los detalles de implementación no
deberían determinar el diseño de los servicios web. Los servicios web
deben verse como puertos a los que enviar mensajes y no como la
interfaz de objetos a los que se accede remotamente (al estilo de
CORBA, DCOM o RMI).
Creación de un servicio web en Visual Studio 2005
La creación de un servicio web ASP.NET desde Visual Studio .NET resulta extremadamente
fácil:
using System;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
[WebService(Namespace = "http://csharp.ikor.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class EchoService : System.Web.Services.WebService
{
public EchoService () {
}
[WebMethod]
public string Echo (string str)
{
return str;
}
}
Código fuente del ejemplo
Una vez que tenemos creado el servicio web, para probarlo lo único que tenemos que hacer es
acceder a él ejecutando nuestro proyecto. Al ejecutar el proyecto, se lanza el servidor de
desarrollo ASP.NET en alguno de los puertos libres que tenga nuestra máquina:
Para acceder al servicio web, no tenemos más que teclear la URL correspondiente al fichero
.asmx en la barra de direcciones de nuestro navegador web. Desde la URL de nuestro servicio
web podemos explorar las distintas operaciones permitidas por el servicio web:
Incluso podemos probar el funcionamiento de nuestro servicio web utilizando los formularios
HTML que automáticamente genera el Internet Information Server para ver qué respuesta que
nos da (en formato XML, como es natural):
El servidor de desarrollo ASP.NET nos ofrece toda la información que podemos necesitar para
utilizar nuestro servicio web, desde los distintos formatos de mensajes que podemos emplear
para utilizarlo hasta su especificación WSDL. Para ello, no tenemos más que acceder a la URL
de nuestro servicio web añadiendo el parámetro ?WSDL:
http://localhost:1881/WebServiceExample/EchoService.asmx?WSDL
Creación de un servicio web en Visual Studio 2003
La creación de un servicio web ASP.NET desde Visual Studio .NET resulta extremadamente
fácil:
A la hora de implementar el servicio web de forma adecuada, lo único que debemos hacer
siempre es asegurarnos de asociarle un espacio de nombres adecuado (por defecto, el espacio
de nombres será http://tempuri.org/). Así mismo, también resulta aconsejable añadirle
una descripción al servicio utilizando el parámetro Description del atributo [WebMethod].
Una vez que tenemos creado el servicio web, para probarlo lo único que tenemos que hacer es
acceder a él a través del Internet Information Server:
Cuando instalamos un servicio web en el IIS, para acceder a él no tenemos más que teclear la
URL correspondiente al fichero .asmx en la barra de direcciones de nuestro navegador web
(p.ej. podríamos escribir http://elvex.ugr.es:800/WebService/Service.asmx si
nuestra máquina fuese elvex.ugr.es y el IIS estuviese funcionando en el puerto TCP 800 en
vez del puerto 80). Desde la URL de nuestro servicio web podemos explorar las distintas
operaciones permitidas por el servicio web:
Incluso podemos probar el funcionamiento de nuestro servicio web utilizando los formularios
HTML que automáticamente genera el Internet Information Server para ver qué respuesta que
nos da (en formato XML, como es natural):
La invocación de un servicio web, por tanto, se puede realizar igual que si utilizásemos un
formulario HTML, pasándole los parámetros mediante una solicitud HTTP POST o empleando
incluso una solicitud de tipo HTTP GET. En este último caso, los parámetros se codifican en la
URL de la solicitud, p.ej.
http://elvex.ugr.es:800/WebService/Service.asmx/Hola?
http://elvex.ugr.es:800/WebService/Service.asmx/Convertir?tempFahrenheit=100
El IIS nos ofrece toda la información que podemos necesitar para utilizar nuestro servicio web,
desde los distintos formatos de mensajes que podemos emplear para utilizarlo hasta su
especificación WSDL, a la que podemos acceder mediante la URL ...asmx?WSDL:
http://localhost:800/WebService/Service.asmx?WSDL
Tutorial: Uso de servicios web
Para poder utilizar un servicio web, del que usualmente sólo tendremos su especificación
WSDL, tenemos que implementar el cliente que accederá al servicio web. En vez de
implementar manualmente el cliente, podemos emplear la utilidad wsdl.exe que genera los
proxies necesarios a partir de la descripción WSDL.
Desde dentro del entorno, el acceso a un servicio web es aún más simple. Sólo tenemos que
agregar una referencia web a nuestro proyecto y el Visual Studio .NET se encargará de crear
las clases necesarias para acceder al servicio web como si se tratase de un acceso local a los
servicios ofrecidos por cualquier otra de las clases de nuestra aplicación. Para ver cuáles son
las clases generadas automáticamente por el Visual Studio .NET sólo tenemos que pinchar en
el botón "Mostrar todos los archivos" del Explorador de Soluciones.
Si queremos que cualquier tráfico dirigido hacie el exterior pase por el proxy,
podemos utilizar la propiedad Select de la clase auxiliar GlobalProxySelection
incluida en el espacio de nombres System.Net. Por ejemplo:
System.Net.GlobalProxySelection.Select =
new System.Net.WebProxy("http://stargate.ugr.es:3128/",true);
Si sólo tenemos que utilizar el proxy para acceder a un servicio web concreto,
podemos fijar la propiedad Proxy del objeto que represente el servicio web al que
queremos acceder, tal como aparece en el siguiente fragmento de código:
using System.Net;
...
En este caso, sólo se utiliza el proxy para acceder al servicio web cuya propiedad
Proxy hayamos establecido. El resto del tráfico generado por nuestra aplicación no
se pasará a través del proxy.
Una de las ventajas de utilizar servicios web es que éstos facilitan la interoperabilidad entre
aplicaciones escritas de forma independiente utilizando distintos lenguajes y entornos de
programación.
String endpointURL =
"http://elvex.ugr.es:800/WebService/Service.asmx";
String namespace = "http://elvex.ugr.es/";
String methodName;
methodName = "Hola";
// Servicio de eco
methodName = "Eco";
System.out.println(
call3.invoke( new Object[] {"Prueba de eco... OK" } ));
methodName = "Convert";
Integer iresponse;
} catch(Exception error){
System.out.println(error);
}
}
}
Importante
Aunque el protocolo SOAP en sí es el mismo en las distintas implementaciones
existentes, existen distintas formas de codificar los mensajes SOAP, algo que
deberemos tener en cuenta para construir sistemas interoperables en la práctica.
Por defecto, en la plataforma .NET se utilizan mensajes SOAP de tipo
"document/literal" (que representan los datos utilizando serialización XML y no
utiliza el estándar SOAP/RPC para realizar las llamadas a los servicios web). Por otra
parte, Apache Axis emplea, por defecto, mensajes SOAP "RPC/encoded" (que utiliza
el estándar SOAP para establecer una correspondencia entre los tipos de datos y su
representación en XML, así como el estándar definido en la especificación SOAP para
realizar llamadas a métodos). Este hecho deberemos tenerlo en cuenta a la hora de
implementar servicios web en distintas plataformas. En la práctica, basta con utilizar
el atributo [SoapRpcService] en la implementación .NET.
Ejercicio
Comprobar las diferencias existentes entre los formatos SOAP
"document/literal" y SOAP "RPC/encoded". Para ello, pruebe con
métodos etiquetados con [WebMethod] que reciban parámetros de
distintos tipos (por ejemplo, fechas y otros tipos de objetos).
Caso práctico: Servicios web de Google
Ahora veremos cómo utilizar el buscador más famoso de Internet en nuestras propias
aplicaciones.
Comenzamos creando un nuevo "sitio web" de tipo "aplicación web ASP.NET" con Visual C#:
Para que el proyecto quede tal como aparece en la figura, nos hace falta agregar la referencia
web que nos permitirá acceder a los servicios web de Google. Para ello, usamos la descripción
WSDL que se encuentra en http://api.google.com/GoogleSearch.wsdl
Una vez que hemos añadido la referencia web, para acceder a los servicios web de Google sólo
tendreemos que incluir la sentencia using
com.google.api en la cabecera de nuestros ficheros de código (en Visual Studio 2002/2003,
el servicio web se importaba dentro del espacio de nombres de nuestra aplicación, por lo que
la sentencia debía ser using
GoogleClient.com.google.api).
A continuación, diseñamos el formulario web para que quede como en la siguiente imagen:
Para que el formulario quede tal como aparece en la figura, hemos añadido algunos controles
estándar a nuestro formulario: una imagen ( Image), una caja de texto (TextBox) y un botón
(Button). A continuación, establecemos las siguiente propiedades para los controles de
nuestro formulario:
if ( Page.IsPostBack )
query = TextBoxSearch.Text;
else
query = Request.Params["query"];
La rutina encargada de hacer la búsqueda accede al servicio web utilizando nuestra clave de
acceso, la cual nos permite hacer 1000 búsquedas diarias. Con el resultado devuelto por
Google creamos un documento XML, al cual le aplicaremos una hoja de estilo XSLT para
generar la salida en HTML. La implementación en C# resulta algo tediosa pero es fácil de
entender:
private void Search (string query)
{
GoogleSearchService service = new GoogleSearchService();
GoogleSearchResult result;
// GUI
TextBoxSearch.Text = query;
// Consulta
result = service.doGoogleSearch
(key, query, start, maxResults, filter, restrict, safeSearch, lr, "", "");
// Resultado
writer.Formatting = Formatting.Indented;
writer.Indentation = 2;
writer.WriteStartDocument();
writer.WriteStartElement("SearchResults");
writer.WriteAttributeString("searchComments", result.searchComments);
writer.WriteAttributeString( "estimatedTotalResultsCount",
result.estimatedTotalResultsCount.ToString());
writer.WriteAttributeString( "searchQuery", result.searchQuery);
writer.WriteAttributeString( "startIndex", result.startIndex.ToString());
writer.WriteAttributeString( "endIndex", result.endIndex.ToString());
writer.WriteAttributeString( "searchTips", result.searchTips);
writer.WriteAttributeString( "searchTime", result.searchTime.ToString());
writer.WriteStartElement("ResultSet");
writer.WriteStartElement("Item");
writer.WriteStartElement("summary");
writer.WriteString(element.summary);
writer.WriteEndElement();
writer.WriteStartElement("URL");
writer.WriteString(element.URL);
writer.WriteEndElement();
writer.WriteStartElement("snippet");
writer.WriteString(element.snippet);
writer.WriteEndElement();
writer.WriteStartElement("title");
writer.WriteString(element.title);
writer.WriteEndElement();
writer.WriteStartElement("directoryTitle");
writer.WriteString(element.directoryTitle);
writer.WriteEndElement();
writer.WriteEndElement();
}
writer.WriteEndElement(); // ResultSet
writer.WriteEndElement(); // SearchResults
writer.WriteEndDocument();
writer.Flush();
stream.Position = 0;
xml.Load(stream);
xslt.Load(Server.MapPath("search.xsl"));
xslt.Transform(xml,null,html);
// Salida final
html.Position=0;
Lo único que nos falta es crear la transformación XSLT que nos permita visualizar el resultado
de la búsqueda en HTML. También resulta algo larga, pero no debería ser difícil de interpretar
si se tienen unos conocimientos mínimos de XML y XSLT:
<center>
<table width="90%" border="0">
<tr bgcolor="#3366cc">
<td>
<font color="#ffffff">Páginas con
<b>"<xsl:value-of select="@searchQuery"/>".</b></font>
</td>
<td width="20%" align="center">
<font color="#ffffff">
Resultados
<xsl:value-of select="@startIndex"/> - <xsl:value-of select="@endIndex"/>
de <xsl:value-of select="@estimatedTotalResultsCount"/>.
</font>
</td>
<td width="20%" align="center">
<font color="#ffffff">
Tiempo de búsqueda: <xsl:value-of select="@searchTime"/> segundos.
</font>
</td>
</tr>
<xsl:for-each select="ResultSet/Item">
<tr>
<td colspan="3">
<a>
<xsl:attribute name="href">
<xsl:value-of select="URL"/>
</xsl:attribute>
</a>
</td>
</tr>
<tr>
<td colspan="3">
<xsl:value-of disable-output-escaping="yes" select="snippet"/>
</td>
</tr>
<tr>
<td colspan="3">
<font color="green"> <xsl:value-of select="URL"/> </font>
<br></br>
</td>
</tr>
</xsl:for-each>
</table>
</center>
</xsl:if>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
¡Voilà! Hemos terminado una aplicación web que utiliza el Google como motor de búsqueda:
Código fuente del cliente de Google (aplicación web para Visual Studio 2003)
Código fuente del cliente de Google (sitio web para Visual Studio 2005)
Código fuente del cliente de Google (aplicación web para Visual Studio 2005)
Código fuente del cliente de Google (aplicación web para Visual Studio 2008)
Como es lógico, podemos acceder a un servicio web desde una aplicación de cualquier tipo, no
sólo desde aplicaciones web ASP.NET. Por ejemplo, podemos usar los servicios web ofrecidos
por Google desde una aplicación Windows. Para ello, sólo tenemos que agregar una referencia
web a nuestro proyecto, p.ej.
Código fuente del cliente de Google (como aplicación Windows, para Visual Studio 2008)
Caso práctico: Servicios web de Amazon
Ahora construiremos una sencilla aplicación de e-business, para lo cual accederemos a los
servicios web de Amazon, la librería más grande de Internet, para lo cual comenzamos
creando una aplicación web ASP.NET.
En primer lugar, tenemos que conseguir la descripción WSDL de los servicios web de Amazon
para poder utilizarlos en nuestro proyecto. De entre los muchos servicios web ofrecidos por
Amazon para realizar diferentes tareas, nosotros usaremos A2S (Amazon Associates Service).
La descripción WSDL de este servicio web ofrecido por Amazon se puede encontrar en la URL
http://ecs.amazonaws.com/AWSECommerceService/AWSECommerceService.wsdl o, de formal
alternativa, también la podemos encontrar en
http://webservices.amazon.com/AWSECommerceService/AWSECommerceService.wsdl.
Como tenemos que acceder a los servicios web de Amazon desde nuestra aplicación,
necesitamos crear las clases que harán de proxy en nuestra aplicación cliente. Esto lo podemos
hacer utilizando la utilidad wsdl.exe:
o bien añadiendo una referencia web a nuestro proyecto de aplicación web ASP.NET:
Hecho esto, si llamamos AmazonClient al formulario web que utilizaremos para acceder al
catálogo de libros de Amazon, nuestro proyecto debería aparecer en el explorador de
soluciones tal como muestra la siguiente captura de pantalla:
A continuación, diseñamos nuestro formulario web con una tabla (menú "Tabla > Insertar"). Al
formulario le añadiremos un par de botones, una imagen en la que mostraremos la portada del
libro (ImagenLibro) y tres etiquetas en las que se visualizarán el título del libro, su autor y su
precio (LabelTitulo, LabelAutor y LabelPrecio, respectivamente).
Antes de que se nos olvide, al fichero de código asociado al formulario hemos de añadirle la
sentencia using que nos permitirá acceder a los servicios ofertados por Amazon:
using Amazon.com.amazonaws.ecs;
si teníamos Amazon como espacio de nombres predeterminado para nuestro proyecto en
Visual Studio, o bien
using com.amazonaws.ecs;
Aparte de los controles de nuestra interfaz, necesitaremos las siguientes variables de instancia
en nuestro formulario ASP.NET:
// Servicio web
AWSECommerceService service;
// Cesta de la compra
Cart cart;
Para seleccionar un libro utilizaremos su ISBN [International Standard Book Number], que
pasaremos como parámetro a nuestro formulario web cuando queramos consultar los datos de
un libro concreto en Amazon:
if (isbn == String.Empty)
isbn = "0321113594";
// Solicitud
// Parámetros de la búsqueda
request.SearchIndex = "Books";
request.Power = "ISBN: " + isbn;
// Resultados deseados
// Búsqueda de artículos
search.AssociateTag = "ikorbooks-20";
search.AWSAccessKeyId = "0PDW1BZN4N11TAXXS6G2";
search.Request = new ItemSearchRequest[] { request };
// Ejecución de la consulta
// Resultado de la consulta
ImagenLibro.ImageUrl = book.MediumImage.URL;
LabelTitulo.Text = book.ItemAttributes.Title;
int i;
int autores = (book.ItemAttributes.Author != null) ?
book.ItemAttributes.Author.Length : 0;
LabelAutor.Text = "";
LabelAutor.Text += book.ItemAttributes.Author[i];
if (i < autores - 2) {
LabelAutor.Text += ", ";
} else if (i == autores - 2) {
LabelAutor.Text += " & ";
}
}
// Carrito de la compra
cart = (Cart)Session["cart"];
if (cart != null)
LabelCompra.Text = "Tiene " + cart.CartItems.CartItem.Length
+ " libros distintos en su cesta de la compra...";
}
Los accesos a los servicios web de Amazon, en su versión actual, siempre funcionan de la
misma forma:
También hemos de especificar qué información nos interesa acerca del libro que estamos
buscando. Para ello, existen diferentes grupos de respuestas predefinidas que se han de
seleccionar mediante el parámetro RequestGroup de nuestra solicitud. Si sólo quisiéramos
información básica acerca del libro (título, autores, ISBN, etc.), nos bastaría con indicar
"ItemAttributes". Como queremos mostrar una imagen de la portada del libro en nuestra
aplicación, usamos además el grupo "Images". Por último, como también queremos mostrar
información relativa al precio real del libro, no simplemente su precio de catálogo, recurrimos
al grupo "Offers".
Amazon, además de comercializar sus productos, permite que sus asociados vendan sus
productos a través de Amazon Marketplace, motivo por el que podemos encontrarnos diversas
ofertas para un mismo producto. Esto, entre otras muchas cosas, nos permite comprar libros
usados a un precio muy reducido (y en excelente estado), una costumbre muy habitual en
países como EE.UU..
Realización de pedidos
Aún nos falta por implementar la parte de nuesta aplicación que le permitirá al usuario realizar
sus compras a través de nuestra aplicación.
En primer lugar, hemos de implementar el código necesario para que el usuario añada un libro
a su cesta de la compra cuando pulse sobre el botón correspondiente, para lo cual necesitamos
de nuevo acceder al servicio web de Amazon, usando la operación CartCreate cuando el
usuario selecciona un producto por primera vez y la operación CartAdd cuando el usuario ya
tiene un carrito de la compra al que quiere añadirle nuevos productos.
cartCreateRequestItem.ASIN = isbn;
cartCreateRequestItem.Quantity = "1";
cartCreateRequestItem.AssociateTag = "ikorbooks-20";
create.AssociateTag = "ikorbooks-20";
create.AWSAccessKeyId = "0PDW1BZN4N11TAXXS6G2";
create.Request = new CartCreateRequest[] { request };
cart = response.Cart[0];
} else {
cartAddRequestItem.ASIN = isbn;
cartAddRequestItem.Quantity = "1";
cartAddRequestItem.AssociateTag = "ikorbooks-20";
add.AssociateTag = "ikorbooks-20";
add.AWSAccessKeyId = "0PDW1BZN4N11TAXXS6G2";
add.Request = new CartAddRequest[] { request };
cart = response.Cart[0];
}
if (cart != null) {
Session["cart"] = cart;
LabelCompra.Text = "Tiene " + cart.CartItems.CartItem.Length
+ " libros en su cesta de la compra...";
}
}
Finalmente, para que nuestra aplicación de comercio electrónico sea operativa, sólo nos falta
dejar que el usuario pueda pasar por caja cuando lo desea, proceso del cual se encargará
Amazon en la URL a la que redireccionamos a nuestro cliente:
Y con esto ya tenemos una atractiva página que le permite al usuario comprar sus libros
favoritos por Internet:
Código fuente del cliente de Amazon (aplicación web para Visual Studio 2005)
Código fuente del cliente de Amazon (aplicación web para Visual Studio 2008)
Servicios web de Amazon
En 2008, Amazon reemplazó su servicio web de comercio electrónico
ECS (Electronic Commerce Service) por A2S (Amazon Associates
Service), que corresponde a la versión 4 de ECS. Se puede obtener
más información acerca de este y otros servicios web de Amazon en la
siguiente URL: http://aws.amazon.com/.
Diseño de arquitecturas software
Arquitecturas multicapa
Los protocolos de red también se diseñan utilizando distintas capas: la capa de aplicación
(HTTP) utiliza los servicios de la capa de transporte (TCP), la cual se implementa sobre la capa
de red (IP) y así sucesivamente hasta llegar a la transmisión física de los datos a través de
algún medio de transmisión.
El problema con esta descomposición es que la lógica de la aplicación suele acabar mezclada
con los detalles de la interfaz de usuario, dificultando las tareas de mantenimiento a que todo
software se ve sometido y destruyendo casi por completo la portabilidad del sistema, que
queda ligado de por vida a la plataforma para la que se diseñó su interfaz en un primer
momento.
La solución, por tanto, pasa por crear nueva capa en la que se separe la lógica de la aplicación
de la interfaz de usuario y del mecanismo utilizado para el almacenamiento de datos. El
sistema resultante tiene tres capas:
Cuando el usuario del sistema no es un usuario humano, se hace evidente la similitud entre las
capas de presentación y de acceso a los datos. Teniendo esto en cuenta, el sistema puede
verse como un núcleo (lógica de la aplicación) en torno al cual se crean una serie de interfaces
con entidades externas. Esta vista simétrica del sistema es la base de la arquitectura
hexagonal de Alistair Cockburn.
No obstante, aunque sólo fuese por las peculiaridades del diseño de interfaces de usuario,
resulta útil mantener la vista asimétrica del software como un sistema formado por tres capas.
Además, suele ser recomendable diferenciar lo que se suministra (presentación) de lo que se
consume (acceso a los servicios suministrados por otros sistemas).
A pesar del atractivo de esta arquitectura con tres capas (basta con pensar lo que facilitaría la
conversión de aplicaciones Windows en aplicaciones web), esta arquitectura no se ha impuesto
del todo porque las herramientas de desarrollo suelen estar diseñadas para construir
aplicaciones cliente/servidor ligadas a algún productor de software. De hecho, puede resultar
difícil (e incluso imposible) descomponer un sistema en tres capas con determinadas
herramientas de desarrollo.
En el caso de la plataforma .NET, el aspecto final de una arquitectura con tres capas sería algo
así como muestra la siguiente figura:
Referencias
Martin Fowler et al.: Patterns of Enterprise Application Architecture. Addison-
Wesley, 2003. ISBN 0-321-12742-0.
Frank Buschmann, Regine Meunier, Hans Rohnert, Peter Sommerlad & Michael
Stal: Pattern-Oriented Software Architecture. Volume 1. John Wiley & Sons,
1996. ISBN 0-471-95869-7.
Refactorización & Pruebas de unidad
Por lo general, el diseño de la lógica una aplicación se suele ajustar a uno de los tres
siguientes patrones de diseño:
Patrones de diseño
Los patrones de diseño describen soluciones elegantes a problemas específicos que se repiten
en muchas aplicaciones diferentes. Estas soluciones puede que requieran algo más de esfuerzo
que una solución ad hoc del problema que tengamos entre manos, pero este esfuerzo se verá
recompensado con creces si nos permite construir aplicaciones más flexibles y fáciles de
mantener.
Los patrones de diseño nos permiten reutilizar buenos diseños, hacer explícito el conocimiento
del diseño de software (mediante abstracciones de las soluciones encontradas en buenos
diseños) y amplían nuestro vocabulario (usualmente, a cada patrón de diseño se le asocia un
nombre con el que hacerle referencia). En otras palabras, los patrones de diseño nos permiten,
no sólo mejorar nuestros diseños, sino ampliar nuestras expectativas de aprendizaje y nuestra
capacidad de comunicación con otros diseñadores.
Refactorización
Una refactorización [refactoring] es un cambio realizado a la estructura interna del software sin
modificar su comportamiento observable desde el exterior. Los cambios de este tipo pueden
ser útiles si hacen que el software sea más fácil de comprender y de modificar cuando hemos
de adaptarlo a nuevas necesidades. Refactorizar es reestructurar el software aplicando una
serie de refactorizaciones sin modificar su comportamiento. Cuando se realiza esta tarea de
forma adecuada, lo que estamos haciendo es mejorar la calidad de nuestros diseños (para lo
cual muchas veces echaremos mano de patrones de diseño). En otras palabras, aunque no
estemos añadiéndole funciones al programa (su valor actual), estamos facilitando su desarrollo
futuro.
Pruebas de unidad
Cuando se implementa software, resulta recomendable comprobar que el código que hemos
escrito funciona correctamente. Para ello implementamos tests que verifican que nuestro
programa genera los resultados que de él esperamos. Conforme vamos añadiéndole nueva
funcionalidad a nuestras aplicaciones, creamos nuevos tests con los que podemos medir
nuestros progresos y comprobar que lo que antes funcionaba sigue funcionando (test de
regresión). Las pruebas de unidad también son de vital importancia cuando refactorizamos:
aunque no añadimos nueva funcionalidad, estamos modificando la estructura interna de
nuestro programa y debemos comprobar que no introducimos errores. Las pruebas de unidad
son, por tanto, muy importantes en el desarrollo de software.
Para agilizar las pruebas resulta recomendable que un test sea completamente automático y
compruebe los resultados esperados. No es muy apropiado llamar a una función, guardar el
resultado en algún sitio y después tener que comprobar manualmente si el resultado era el
deseado. En la práctica, mantener automatizado un conjunto amplio de tests permite reducir el
tiempo que se tarda en depurar errores y en verificar la corrección del código. De hecho,
existen herramientas especialmente diseñadas para implementar y automatizar la realización
de pruebas de unidad (NUnit en la plataforma .NET y JUnit en Java, por ejemplo).
Incluso hay quien defiende que, antes de comenzar a escribir código se deben implementar los
tests que nos permitirán comprobar que el código funciona correctamente [TDD: Test-Driven
Development]. Aunque a primera vista pueda parecer extraño, de esta forma nos podemos
centrar mas fácilmente en qué necesitamos hacer para que nuestra aplicación funcione,
hacemos hincapié en la interfaz de nuestro sistema antes que en su implementación (algo
siempre bueno) y definimos claramente cuándo termina nuestro trabajo (cuando se pasan con
éxito todos los tests).
Bibliografía de interés
Frank Buschmann, Regine Meunier, Hans Rohnert, Peter Sommerlad & Michael
Stal: Pattern-Oriented Software Architecture. Volume 1. John Wiley & Sons,
1996. ISBN 0-471-95869-7.
Alistair Cockburn: Agile Software Development. Addison-Wesley, 2002. ISBN
0-201-69969-9.
Martin Fowler et al.: Refactoring: Improving the design of existing code.
Addison-Wesley, 2000. ISBN 0-201-48567-2.
Martin Fowler et al.: Patterns of Enterprise Application Architecture. Addison-
Wesley, 2003. ISBN 0-321-12742-0.
Erich Gamma, Richard Helm, Ralph Johnson & John Vlissides: Design
Patterns: Elements of reusable object-oriented software. Addison-Wesley,
1994. ISBN 0-201-63361-2.
Robert L. Glass: Facts and Fallacies of Software Engineering. Addison-Wesley,
October 2002. ISBN 0-321-11742-5.
Caso práctico: Biblioteca digital
En esta sección, intentaremos mostrar la importancia de las técnicas descritas en el desarrollo
de software de calidad mediante un ejemplo concreto, aun a riesgo de que la simplicidad del
mismo haga parecer triviales y de poco valor los pasos que iremos dando. Es de recibo resaltar
que, en un proyecto real, la utilidad y la importancia de las técnicas empleadas crece de forma
proporcional a la dimensión del problema.
Diseño inicial
Tomaremos como ejemplo ilustrativo el diseño de una biblioteca digital. En dicha biblioteca los
documentos se clasifican por categorías. Éstas categorías se organizan de forma jerárquica.
Además, permitimos que los usuarios de la biblioteca escriban sus propios comentarios acerca
de los documentos de la biblioteca y evalúen éstos en función de sus gustos.
Dados los requerimientos del problema, un diseño inicial de las clases de nuestro sistema
podría ser como el siguiente:
A partir del diagrama anterior, podemos escribir el esqueleto del código en C# de las cinco
clases principales que inicialmente formarán parte de nuestro sistema. Para ello, creamos una
biblioteca de clases llamada DigitalLibrary.dll. Aunque podríamos directamente crear
clases con atributos públicos, esto no resulta demasiado correcto para mantener la
encapsulación de los distintos módulos de nuestro sistema, por lo que crearemos variables de
instancia privadas y propiedades públicas para acceder a ellas (sin olvidar los comentarios XML
para documentar nuestro código, que aquí omitimos por brevedad).
La clase Category es algo más compleja, pues debemos mantener la jerarquía de categorías
de una forma apropiada, para lo cual utilizamos la clase ArrayList del espacio de nombres
System.Collections:
// Autores
// Categorías
A continuación, implementamos la clase Reviewer, que sirve para modelar a los usuarios
registrados del sistema, aquéllos que pueden escribir comentarios acerca de los documentos
de nuestra biblioteca digital:
Los aspectos más destacables de la clase anterior son, en primer lugar, la forma de trabajar
con las claves de acceso, que nunca se deben almacenar sin encriptar, y, en segundo lugar, el
uso de excepciones para indicar la ocurrencia de errores.
Finalmente, sólo nos queda por implementar la clase Review, que inicialmente será una clase
inmutable:
public Review
(Reviewer reviewer, Document document, string
text, Rating rating)
{
this.reviewer = reviewer;
this.document = document;
this.text = text;
this.date = DateTime.Now;
this.rating = rating;
}
En este caso, hemos definido una enumeración para especificar el conjunto de valores
permitido al evaluar un documento.
Pruebas de unidad
Una vez que tenemos implementadas las clases que servirán de base a nuestra biblioteca
digital, resulta recomendable que escribamos algunas pruebas de unidad. Éstas nos servirán
para comprobar que nuestra aplicación funciona correctamente y, conforme vayamos
añadiéndole nuevas funciones, las pruebas de unidad nos servirán de comprobación de que
todo sigue funcionando correctamente.
Para implementar las pruebas de unidad, resulta recomendable utilizar alguna heramienta tipo
xUnit. Dichas herramientas están diseñads para facilitarnos la automatización de las pruebas.
En el caso de la plataforma .NET, la utilidad se denomina NUnit y la podemos descargar
gratuitamente de http://www.nunit.org.
Para implemetar pruebas de unidad con NUnit, creamos una nueva biblioteca de clases en la
solución correspondiente a nuestra aplicación. Por convención, su nombre estará formado por
el nombre de la biblioteca que estemos probando seguido de Test. Es este caso, la biblioteca
que albergará los tests será, por tanto, DigitalLibraryTest.dll. Dicha biblioteca incluirá
una referencia a la biblioteca de clases donde tenemos implementadas las clases de nuestra
aplicación:
Así mismo, también deberemos incluir una referencia a la DLL donde están definidos los
atributos utilizados por NUnit, los cuales veremos cómo funcionan en unos momentos.
Seleccionamos la opción "Agregar referencia..." y, a continuación, pinchamos sobre el botón
"Examinar...". Hemos de localizar la DLL denominada nunit.framework.dll que se halla en
el subdirectorio bin del directorio donde hayamos instalado NUnit (usualmente, en
C:\Archivos de Programa\NUnit...):
Una vez añadidas las dos referencias (al proyecto DigitalLibrary.dll y a la biblioteca
nunit.framework.dll), podemos comenzar a escribir nuestras primeras pruebas de unidad:
using System;
using NUnit.Framework;
namespace DigitalLibrary
{
[TestFixture]
public class CategoryTest
{
[Test]
public void HierarchyTest ()
{
Category software = new Category("Software");
Category lenguajes = new Category("Lenguajes de
programación");
software.AddSubCategory(lenguajes);
Cada prueba de unidad es un método etiquetado con el atributo [Test]. Dicho método ha de
ser necesariamente un procedimiento (sin parámetros ni valor de retorno). Usualmente, dichos
métodos se encargarán de inicializar un conjunto de objetos, invocar algunos de sus métodos
y, finalmente, comprobar el estado de los objetos mediante aserciones utilizando la clase
NUnit.Framework.Assertion.
Para ejecutar las pruebas de unidad, podemos utilizar la utilidad NUnit-Gui que viene incluida
en la distribución de NUnit:
Su funcionamiento es muy sencillo. Sólo tenemos que abrir el fichero que contiene nuestras
pruebas de unidad (menú "File", opción "Open...") y pulsar el botón "Run". Sorprendentemente
(o quizá no tanto), la simple prueba nos da un error:
NUnit nos muestra en rojo los nodos correspondientes a tests que no se han ejecutado con
éxito. En los paneles de la derecha podemos buscar las posibles causas de los errores.
Analizando la situación en la que se ha producido nuestro error, nos podemos dar cuenta del
origen del error: al implementar las propiedades SuperCategories y SubCategories de la
clase Category no tuvimos en cuenta que deseamos obtener un vector de categorías (no de
simples objetos), por lo que debemos corregir la implementación "errónea" de estas
propiedades (y otras análogas). Aparte de corregir el error concreto, es deseable que
corrijamos errores similares allá donde aparezcan, por lo que buscaremos los usos del método
ToArray de la clase ArrayList y escribiremos algo similar a lo siguiente:
get
{
return (Category[]) XXX.ToArray ( typeof(Category) );
}
Una vez realizada la corrección, recompilamos y volvemos a ejecutar las pruebas de unidad
con NUnit. Ahora sí obtenemos luz verde para seguir avanzando:
Es conveniente que completemos nuestros tests para asegurarnos de que nuestra
implementación funciona correctamente. Por ejemplo, podemos comprobar mejor el
funcionamiento de nuestra clase Category construyendo una jerarquía simple. Los siguientes
tests nos dan una idea más realista de los conjuntos de pruebas típicos en aplicaciones reales:
using System;
using NUnit.Framework;
namespace DigitalLibrary
{
///
/// Pruebas de unidad para la clase Category
///
[TestFixture]
public class CategoryTest
{
Category software;
Category lenguajes;
Category csharp;
Category delphi;
Category java;
[SetUp]
public void Init()
{
// Categorías
software = new Category("Software");
lenguajes = new Category("Lenguajes de programación");
csharp = new Category("C#");
delphi = new Category("Delphi");
java = new Category("Java");
// Jerarquía
software.AddSubCategory(lenguajes);
lenguajes.AddSubCategory(csharp);
lenguajes.AddSubCategory(delphi);
lenguajes.AddSubCategory(java);
}
[Test]
public void TopHierarchyTest ()
{
Assert.AreEqual(0, software.SuperCategories.Length );
}
[Test]
public void SingleSubcategoryTest ()
{
Assert.AreEqual(1, software.SubCategories.Length );
Assert.AreEqual(lenguajes, software.SubCategories[0]);
}
[Test]
public void SingleSupercategoryTest ()
{
Assert.AreEqual(1, lenguajes.SuperCategories.Length);
Assert.AreEqual(software, lenguajes.SuperCategories[0]);
}
[Test]
public void MultipleSubcategoryTest ()
{
Assert.AreEqual(3, lenguajes.SubCategories.Length);
Assert.IsTrue (lenguajes.isSubCategory(csharp));
Assert.IsTrue (lenguajes.isSubCategory(delphi));
Assert.IsTrue (lenguajes.isSubCategory(java));
}
[Test]
public void BottomHierarchyTest()
{
Assert.AreEqual(0, csharp.SubCategories.Length);
Assert.AreEqual(0, delphi.SubCategories.Length );
Assert.AreEqual(0, java.SubCategories.Length );
}
[Test]
public void TransitivityTest ()
{
Assert.AreEqual( software,
csharp.SuperCategories[0].SuperCategories[0] );
}
}
}
Aunque no lo hayamos empleado, los distintos métodos estáticos de la clase Assert incluyen
la posibilidad de incluir un parámetro adicional de tipo cadena. Dicho parámetro podemos
utilizarlo para mostrar un mensaje informativo adecuado si se produce un error al ejecutar el
caso de prueba.
En determinados proyectos, puede sernos muy útiles la creación de tests como los anteriores
para definir un criterio de aceptación para nuestra implementación, además de ofrecernos
ciertas garantías de que nuestro código hace lo que tiene que hacer (recuérdense los
comentarios relativos a TDD [Test-Driven Development]).
Obviamente, la ejecución correcta de los casos de prueba no quiere decir que nuestra
aplicación esté libre de errores, sólo que ésta funciona adecuadamente para los casos de
prueba que hemos creado. Por tanto, cuanto más exhaustivos sean nuestros casos de prueba,
más confianza podremos tener en la corrección de nuestra aplicación.
Control de versiones
Aparte de establecer un buen conjunto de pruebas de unidad que nos ayuden a verificar la
corrección de nuestro código, resulta más que aconsejable utilizar alguna herramienta de
control de versiones. De hecho, las herramientas de este tipo son vitales en los proyectos que
se realizan en equipo. Las herramientas de control de versiones almacenan en una base de
datos la evolución de los distintos archivos de nuestros proyectos para que podamos acceder,
en cualquier momento, a cualquier versión que haya existido de nuestros ficheros.
Al emplear una herramienta de control de versiones, podremos hacer tantos cambios como
deseemos en el código sin temor a perder nada. Es más, la ausencia de una herramienta de
control de versiones podría provocar que actuásemos de una forma excesivamente
conservadora para no estropear nada que ya funcionase y, de esa forma, perderíamos gran
parte de las ventajas que ofrece tener un conjunto sólido de tests a la hora de mejorar nuestro
diseño (esto es, refactorizar).
A continuación, debemos indicar dónde queremos almacenar los ficheros de nuestro proyecto
dentro de la estructura jerárquica de ficheros del repositorio de la herramienta de control de
versiones:
Una vez seleccionada la ubicación de los archivos de nuestro proyecto dentro del repositorio de
la herramienta de control de versiones, las versiones actuales de dichos archivos quedan
almacenadas en la herramienta de control de versiones y el Visual Studio .NET se encargará de
interactuar con dicha herramienta cuando haga falta. Los ficheros de nuestro proyecto están
protegidos a buen recaudo:
Si intentamos modificar alguno de los ficheros protegidos, el Visual Studio .NET nos da la
oportunidad de poder editarlos (operación conocida como "check-out" en la gestión de la
configuración del software):
En el Explorador de soluciones también podemos ver los ficheros sobre los que estamos
realizando modificaciones (por ejemplo, para añadir nuevos tests):
Cuando ya hemos efectuado las modificaciones pertinentes (y hemos comprobado que las
modificaciones efectuadas son correctas), guardamos las versiones revisadas de nuestros
ficheros en el repositorio de la herramienta de control de versiones para que los demás
integrantes de nuestro proyecto puedan utilizarlas. Para realizar esta operación, conocida
como "check-in", basta con seleccionar la opción "Proteger..." del menú contextual del
Explorador de soluciones:
Refactorización...
Una vez que ya sabemos cómo crear un conjunto sólido de pruebas de unidad y tenemos
nuestros ficheros almacenados de forma segura con una herramienta de control de versiones,
ya podemos atacar el código sin piedad para ir añadiéndole funcionalidad a nuestra aplicación
mejorando su diseño interno conforme nos vaya haciendo falta.
Al añadir nuevas funciones, también crearemos nuevos tests que verfiquen el funcionamiento
correcto de lo que hayamos añadido. Al refactorizar (reestructurar nuestro diseño para que
éste resulte más legible, modular, reutilizable, mantenible o eficiente), utilizaremos los tests
ya disponibles a modo de test de regresión para comprobar que nuestras modificaciones no
introducen errores donde no los había.
O/R Mapping
La primera opción que se nos puede ocurrir cuando diseñamos un sistema orientado
a objetos es utilizar un registros activos, objetos que encapsulan directamente las
estructuras de datos externas (p.ej. las tuplas de las tablas de la base de datos) e
incorporan la lógica del dominio que les corresponda, aparte de las operaciones necesarias
para obtener y guardar objetos en la base de datos.
Algo más adecuado puede resultar el empleo de gateways, clases auxiliares que se
corresponden con las tablas de la base de datos e implementan las operaciones
necesarias para manipular la base de datos [CRUD: Create, Retrieve, Update & Delete].
Estas clases auxiliares nos permiten no mezclar la lógica de la aplicación con el acceso a
los datos externos, tal como sucede si utilizamos registros activos.
La tercera opción (y siempre hay una tercera opción) es la más compleja pero la más
flexible: O/R Mapping. Se basa en establecer una correspondencia entre el modelo
orientado a objetos del dominio y la representación de los distintos objetos en una base
de datos relacional. En las dos alternativas anteriores, los objetos de la aplicación han de
ser conscientes de cómo se representan en la base de datos. En el caso del O/R Mapping,
los objetos pueden ignorar la estructura de la base de datos y cómo se realiza la
comunicación con la base de datos. La inversión de control característica de esta opción
independiza el modelo orientado a objetos del dominio de la capa de acceso a los datos:
se puede cambiar la base de datos sin tener que tocar el modelo orientado a objetos del
dominio y viceversa. De esta forma, se facilita el desarrollo, la depuración y la evolución
de las aplicaciones.
Tablas y clases
En principio, cada clase de nuestra aplicación se traducirá en una tabla en la base de datos, lo
cual indicamos mediante el atributo [DBTable]:
[DBTable("DOCUMENT")]
public class Document {
...
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class DBTableAttribute : Attribute
{
string tableName;
[DBTable("DOCUMENT")]
public class Document {
...
[DBColumn(DbType.Int32)]
public int id;
[DBColumn(DbType.String, Size=256)]
public string title;
...
}
La implementación del atributo [DBColumn] es algo más larga pero sigue siendo bastante
simple:
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class DBColumnAttribute : Attribute
{
string columnName;
DbType type = DbType.String;
int size = 0;
bool nullable = true;
Claves
De las distintas columnas de una tabla, alguna(s) debe(n) formar parte de la clave primaria de
la relación, lo que podemos indicar introduciendo un nuevo atributo [DBKey]:
[DBTable("DOCUMENT")]
public class Document {
...
[DBColumn(DbType.Int32)]
[DBKey]
public int id;
...
}
[AttributeUsage(AttributeTargets.Property |
AttributeTargets.Field)]
public class DBKeyAttribute : Attribute
{
public DBKeyAttribute ()
{
}
}
Claves externas
La existencia de una claves definidas es vital para poder establecer restricciones de integridad
referencial por medio de claves externas. Dichas claves externas las podemos especificar
utilizando un atributo más: [DBForeignKey].
[DBTable("DOCUMENT")]
public class Document
{
...
[DBForeignKey]
private User owner;
}
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class DBForeignKeyAttribute : Attribute
{
private string columnName;
public DBForeignKeyAttribute ()
{
}
class TestReflection
{
[STAThread]
static void Main(string[] args)
{
if (args.Length != 1) {
Console.WriteLine("Uso: TestReflection <assembly>");
return;
}
try {
assembly = Assembly.LoadFrom(args[0]);
ParseAssembly(assembly);
} catch (Exception e) {
Console.WriteLine("No se pudo cargar el assembly " +
args[0]);
Console.WriteLine(e.Message);
}
Console.ReadLine();
}
...
Cuando tenemos el assembly, podemos recorrer los distintas clases que lo forman para ver en
cuáles de ellas disponemos de la información necesaria para almacenar sus instancias en una
base de datos de forma automática (aquéllas para las cuales hemos utilizado el atributo
DBTable):
if (type.IsClass) {
DBTableAttribute table = getTable(type);
if (table!=null) {
Console.WriteLine();
Console.WriteLine("Clase '{0}' [Tabla {1}]",
type.ToString(), table.TableName);
ParseClass(type);
}
}
}
}
En una función auxiliar descubrimos si una clase tiene asociado un atributo de tipo
[DBTable]:
if (dataTable.Length > 0) {
return dataTable[0];
} else {
return null;
}
}
Finalmente, tenemos que analizar cada una de las clases por separado:
// Columnas
// Clave primaria
Dentro de cada clase hemos de examinar sus campos por separado (para ver cómo se
traducirán en la base de datos) y también debemos ver cuál es la clave primaria de la tabla.
Tenemos que recorrer los campos de la clase y ver cómo se traducen a columnas de la base de
datos, pero antes debemos definir un par de funciones auxiliares de utilidad:
Pasemos ahora al análisis de los campos de la clase. La existencia de claves externas hace que
debamos introducir, en la tabla correspondiente a una clase, las columnas pertenecientes a la
clave primaria de la tabla a la que hace referencia la clave externa. Este hecho nos obliga a
implementar la siguiente función de una forma un tanto extraña (un poco más adelante ya
veremos por qué):
if (column.Length > 0) {
Console.WriteLine(
" · " + member.ReflectedType+"."+member.Name + " [" + type
+ "]"+
" -> Columna " + prefix + col.ColumnName + " [" + col.Type
+ "]");
}
} else {
ParseForeignKey (member.Name,type,table,prefix, new
Visitor(ParseMember));
}
}
Dejemos por ahora a un lado el análisis de las claves externas y pasemos a ver cómo se
identifica la clave primaria de la tabla asociada a una clase:
Tanto al analizar las columnas de una tabla como al identificar su clave primaria debemos
tener en cuenta la existencia de claves externas (llamadas a ParseForeignKey en los
fragmentos de código anteriores). Esto se debe a que la presencia de una clave externa entre
los campos de una clase (sea o no dentro de su clave) implica la introducción en la tabla
correspondiente de una serie de columnas que harán referencia a la clave primaria de otra
tabla, la cual estará, a su vez, derivada de una de otra de las clases de nuestro assembly. Por
tanto, habrá que analizar la clase a la que se hace referencia para completar el análisis de la
clase inicial.
Al repetirse este hecho tanto en ParseMember como en ParseKey, hemos optado por utilizar
una variante del patrón de diseño "Visitante" que parametriza nuestra implementación del
análisis de claves externas:
if (type.IsClass) {
DBTableAttribute dataTable = getTable(type);
if (dataTable!=null) {
foreignTable = dataTable[0].TableName;
prefix = prefix+name+"_";
Con esto conseguimos recorrer la información relativa a una clase que nos permite saber cómo
se representarán las instancias de esa clase en la base de datos:
Veamos un ejemplo para comprender mejor la traducción del conjunto de clases que forman
modelo orientado a objetos al conjunto de tablas que constituyen la base de datos en la que se
almacenarán las instancias de nuestras clases cuando existen referencias de una clase a otra:
[DBTable("ACCESS")]
public class User
{
[DBColumn("id", DbType.Int32)]
[DBKey]
public int id;
[DBTable("DOCUMENT")]
public class Document {
[DBColumn("id",DbType.Int32)]
[DBKey]
public int id;
[DBForeignKey]
public DocumentUser owner;
}
Es mucho más interesante crear en memoria una estructura de datos que refleje la estructura
de la base de datos asociada a los objetos de nuestra aplicación. De esta forma, podremos
reutilizar dicha estructura con diferentes fines sin necesidad de volver a implementar todo lo
relacionado con la reflexión. Esto es, en la función ParseAssembly de la aplicación descrita
en la sección anterior sólo tendremos que escribir:
El modelo de los objetos en la base de datos lo construiremos a partir de tres clases, que
colocaremos en el espacio de nombres DB.ORmap.Model:
DatabaseColumn
// Constructores
public DatabaseColumn
(MemberInfo member, string name, DbType type)
{
this.member = member;
this.id = member.Name;
if (name!=null)
this.name = name;
else
this.name = member.Name;
this.type = type;
}
public DatabaseColumn
(MemberInfo member, DBColumnAttribute column, string prefix)
{
this.member = member;
this.id = prefix+member.Name;
this.id = id.Replace("_",".");
if (column.ColumnName!=null)
this.name = prefix+column.ColumnName;
else
this.name = prefix+member.Name;
this.type = column.Type;
this.size = column.Size;
this.nullable = column.Nullable;
}
// Propiedades
if (id!=null)
writer.WriteAttributeString("id",id);
if (member!=null)
writer.WriteAttributeString
("member",member.ReflectedType+"."+member.Name);
writer.WriteAttributeString("name",name);
writer.WriteAttributeString("type",type.ToString());
if (size>0)
writer.WriteAttributeString("size",size.ToString());
if (!nullable)
writer.WriteAttributeString("nullable",nullable.ToString());
writer.WriteEndElement();
}
}
DatabaseColumns
Esta clase no es más que una colección de columnas de una tabla y su implementación es algo
tediosa pero simple:
// Constructores
// Propiedades
// Conjunto de columnas
if (member!=null)
writer.WriteAttributeString("member",member.Name);
DatabaseObjectModel
/// <summary>
/// Modelo del objeto tal como se almacena en la base de
datos
/// </summary>
Acto seguido, definimos sus constructores, los cuales no tienen mayor secreto:
/// <summary>
/// Constructor
/// </summary>
/// <param name="type">Tipo de objeto para el cual se
construye el modelo</param>
ParseClass(type);
}
/// <summary>
/// Constructor privado
/// </summary>
/// <param name="table">Nombre de la tabla
destino</param>
/// <param name="member">Miembro de la clase que da
origen a la tabla</param>
Como es habitual, nos hacen falta los métodos y propiedades que dan acceso a las variables
de instancia de la clase: la tabla asociada, las columnas que forman la clave primaria, las
columnas que no forman parte de la clave primaria y el conjunto de claves externas asociado a
la clase que se está modelando.
// Superclase
if ( !isForeignKey(member) ) {
if (column!=null)
list.Add( new DatabaseColumn (member, column, prefix ) );
} else {
list = ParseForeignKey (member, type, prefix, new
Visitor(ParseMember));
}
return list;
}
// Claves externas
if (type.IsClass) {
if (table!=null)
foreignTable = table.TableName;
else
foreignTable = null;
prefix += member.Name+"_";
// Columnas
// ID
DBForeignKeyAttribute fk = getFK(member);
if (fk.ColumnName!=null) {
for (int i=0; i<list.Count; i++)
((DatabaseColumn)list[i]).ColumnName = fk.ColumnName;
}
// Clave externa
if (foreignTable!=null)
addForeignKey ( new
DatabaseColumns(foreignTable,member,list) );
}
return list;
}
En formato XML...
También hemos implementado una rutina que nos permite volcar el modelo en formato XML:
writer.WriteStartElement("Table");
if (member!=null)
writer.WriteAttributeString("member", member.Name);
writer.WriteAttributeString("name", this.table);
writer.WriteStartElement("Key");
Key.ToXML(ref writer);
writer.WriteEndElement();
writer.WriteStartElement("Data");
Data.ToXML(ref writer);
writer.WriteEndElement();
writer.WriteEndElement();
}
writer.WriteStartDocument();
writer.WriteStartElement("DatabaseModel");
writer.WriteAttributeString("type", this.type.ToString());
ToXML(ref writer);
writer.WriteEndElement();
writer.WriteEndDocument();
}
return w.ToString();
}
Métodos auxiliares
La implementación de la clase se completa con una serie de rutinas auxiliares que hacen más
legible el resto del código. Estas rutinas son similares a las empleadas en la sección anterior:
if (table.Length > 0) {
return table[0];
} else {
return null;
}
}
if (column.Length > 0)
return column[0];
else
return null;
}
if (fk.Length > 0)
return fk[0];
else
return null;
}
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
/* SQL DDL para <xsl:value-of select="DatabaseModel/@type" /> */
<xsl:apply-templates/>
COMMIT;
</xsl:template>
<xsl:template match="Table">
<xsl:template match="Key">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="Data">
<xsl:if test="count(column)>0">
<xsl:if test="count(../Key/column)>0">,</xsl:if>
<xsl:apply-templates/>
</xsl:if>
</xsl:template>
<xsl:template match="Key/column">
<xsl:value-of select="@name" />
<xsl:call-template name="type" />not null
<xsl:if test="position()!=last()">,</xsl:if>
</xsl:template>
<xsl:template match="Data/column">
<xsl:value-of select="@name" />
<xsl:call-template name="type" />
<xsl:if test="@nullable='False'">not null</xsl:if>
<xsl:if test="position()!=last()">,</xsl:if>
</xsl:template>
<xsl:template name="pk">
<xsl:if test="count(Key/column)>0">,
primary key(<xsl:for-each select="Key/column">
<xsl:call-template name="list" />
</xsl:for-each>)
</xsl:if>
</xsl:template>
<xsl:template name="fk">
<xsl:for-each select="Foreign-Key">
ALTER TABLE <xsl:value-of select="../@name" /> ADD
foreign key ( <xsl:for-each select="column">
<xsl:call-template name="list" />
</xsl:for-each>
) references <xsl:value-of select="@table" />;
</xsl:for-each>
</xsl:template>
<xsl:template name="list">
<xsl:value-of select="@name" />
<xsl:if test="position()!=last()">,</xsl:if>
</xsl:template>
<xsl:template name="type">
<xsl:if test="@type='Int32'"> INT </xsl:if>
<xsl:if test="@type='Date'"> DATETIME </xsl:if>
<xsl:if test="@type='String'"> VARCHAR
(<xsl:value-of select="@size" />) </xsl:if>
<xsl:if test="@type='Object'"> BLOB SUB_TYPE TEXT </xsl:if>
</xsl:template>
</xsl:stylesheet>
Incluso podemos variar ligeramente la parte final de la hoja de estilo para portar nuestro
sistema a otra base de datos (p.ej. InterBase):
<xsl:template name="type">
<xsl:if test="@type='Int32'"> INTEGER </xsl:if>
<xsl:if test="@type='Date'"> TIMESTAMP </xsl:if>
<xsl:if test="@type='String'"> VARCHAR
(<xsl:value-of select="@size" />) </xsl:if>
<xsl:if test="@type='Object'"> TEXT </xsl:if>
</xsl:template>
NOTA: Si queremos que el script resultante con las sentencias CREATE TABLE en SQL sea
ejecutable directamente, deberemos asegurarnos de que no se intenta crear una clave externa
a una tabla antes de haber creado dicha tabla. Esto se puede conseguir fácilmente realizando
lo que técnicamente se denomina una ordenación topológica del grafo de dependencias. En
cristiano, esto quiere decir que primero se crean las tablas que no tienen referencias externas
a otras tablas y, a continuación, se van creando las tablas cuyas claves externas hacen
referencia a tablas ya creadas.
Para aplicar una hoja de estilo XSLT a un fichero XML podemos utilizar el siguiente programa
escrito en C#:
using System;
using System.IO;
using System.Xml;
using System.Xml.XPath;
using System.Xml.Xsl;
namespace XSLT
{
public class XSLT
{
public static void Main (string[] args)
{
if (args.Length != 2) {
Console.Error.WriteLine("Uso: XSLT <fichero XML> <fichero
XSLT>");
return;
}
xslt.Load(stylesheet);
En esta sección vamos a ver cómo podemos implementar un conjunto de rutinas que nos
permitirán leer objetos directamente de la base de datos con sólo especificar su clave primaria
(¡y sin tener que escribir nada de código!). Por ejemplo, podríamos leer un documento de la
base de datos si sabemos cuál es su identificador (666):
Difícilmente se nos puede ocurrir una forma más sencilla de realizar la comunicación entre
nuestra aplicación y la base de datos. Pasemos ahora a ver cómo podemos lograr que nuestro
"servicio de persistencia" funcione correctamente.
namespace DB
{
public interface Database
{
IDbConnection newConnection ();
A continuación se muestra cómo podemos definir una clase que implementa el interfaz anterior
y nos permite acceder a una base de datos utilizando OLE DB. Sólo tenemos que tener en
cuenta la conversión de tipos entre System.Data.DbType y los tipos de datos soportados por
nuestra base de datos:
La siguiente clase se encarga de construir dinámica las sentencias SQL adecuadas para
acceder a la base de datos a partir del modelo de la base de datos de
DatabaseObjectModel:
if (model.Key.Size>0) {
sql += model.Key[0].ColumnName;
if (model.Key.Size>0)
sql += ", ";
sql += model.Data[0].ColumnName;
return sql;
}
return sql;
}
if (model.Key.Size>0) {
where = " WHERE "+model.Key[0].ColumnName+"=?";
return where;
}
El servicio de persistencia
Una vez independizada nuestra implementación de la base de datos utilizada, podemos pasar a
implementar nuestro servicio de persistencia: el módulo de nuestro sistema que nos permitirá
leer, guardar, cambiar y borrar objetos de la base de datos.
/// Constructor
if (model.Data.Size > 0) {
ParameterList(selectCommand,model.Key, key);
connection.Open();
try {
if (reader.Read())
data = DataObjects(reader);
reader.Close();
reader = null;
if (data!=null)
obj = factory.Create(type,model,data);
}
return obj;
}
ParameterList(deleteCommand,model.Key, obj);
connection.Open();
try {
deleteCommand.ExecuteNonQuery();
} catch (Exception error) {
Console.Error.WriteLine(error);
}
connection.Close();
}
ParameterList(insertCommand,model.Key, obj);
ParameterList(insertCommand,model.Data,obj);
connection.Open();
try {
insertCommand.ExecuteNonQuery();
} catch (Exception error) {
Console.Error.WriteLine(error);
}
connection.Close();
}
// Lectura de datos
return values;
}
// Listas de parámetros
property = obj.GetType().GetProperty(columns[i].MemberName);
if (property!=null) {
param.Value = property.GetValue(obj,null);
} else {
field = obj.GetType().GetField(columns[i].MemberName);
param.Value = field.GetValue(obj);
}
}
}
Con esto ya tenemos implementado el código que dinámicamente se encarga de ejecutar las
sentencias SQL adecuadas sobre la base de datos con la que estemos trabajando. Sólo nos
falta ver cómo podemos instanciar objetos a partir de los datos que leemos de la base de
datos:
Instanciación dinámica de objetos
La clase ObjectFactory nos permite crear objetos a partir de los datos que leemos de la
base de datos:
if (constructor!= null) {
obj = constructor.Invoke(param);
FillColumns(obj, type, model.Key, data, 0);
FillColumns(obj, type, model.Data, data, model.Key.Size);
FillFK(obj, type, model, data);
}
return obj;
}
value = data[offset+i];
if (value!=null
&& columns[i].MemberPath.Equals ( columns[i].MemberName) )
SetValue(obj, type, columns[i].MemberName, value);
}
}
private static void SetValue (object obj, Type type, string name,
object value)
{
FieldInfo field = type.GetField(name);
if (field!=null)
field.SetValue(obj,value);
}
if (key[i]==null)
valid = false;
}
if (valid) {
object fk = service.Find( GetType(type,columns.Member.Name),
key);
SetValue(obj,type,columns.Member.Name,fk);
}
}
if (field!=null){
return field.FieldType;
else
return null;}
}
if ( pos>=0 )
return data[pos];
else
return null;
}
return pos;
}
}
El aspecto más destacable del código anterior es que, al instanciar un objeto, puede que
tengamos que traer otros objetos de la base de datos: si la representación del objeto en la
base de datos incluye una clave externa, esto se traduce en una referencia a otro objeto que
habrá de construirse a partir de su clave primaria (de ahí la llamada a service.Find en el
método FillFK). ¡Ojo! Tal como está, esta implementación no funcionará
correctamente ante la presencia de ciclos.
Desarrollo de componentes
En programación existen diferentes paradigmas. De la programación no estructurada utilizada
inicialmente se pasó a la programación estructurada, en la cual todo se expresa utilizando
únicamente estructuras de control secuenciales, condicionales e iterativas (véase el artículo
"Go to statement considered harmful" en http://www.acm.org/classics/oct95/).
Actualmente, existen distintas plataformas para las cuales se pueden desarrollar componentes.
Cada plataforma define un modelo de componente y suele especificar protocolos para
instanciar y utilizar componentes dentro de un mismo proceso o en procesos diferentes, los
cuales pueden estar en distintas máquinas. A continuación se mencionan las plataformas más
importantes en la actualidad:
Como se puede ver, existen distintas plataformas que coexisten e interactúan. De hecho, la
evolución de unas influye en la evolución de otras. Esta constante evolución hace que sea
imposible predecir el futuro de los componentes como mercado independiente de las
aplicaciones. Lo que sí parecen claros son los beneficios que se obtienen al desarrollar
componentes reutilizables en distintos proyectos (aunque esto se limite a proyectos dentro de
una misma empresa).
Para profundizar...
Los frameworks suelen hacer un uso extensivo de patrones de diseño y su creación requiere
una gran habilidad, destreza y visión de futuro por nuestra parte, además de un amplio
conocimiento del diseño de software mantenible, flexible y extensible.
Para profundizar...
Frank Buschmann, Regine Meunier, Hans Rohnert, Peter Sommerlad & Michael
Stal: Pattern-Oriented Software Architecture - Volume 1: A System of
Patterns. John Wiley & Sons, 1996. ISBN 0-471-95869-7.
Martin Fowler et al.: Patterns of Enterprise Application Architecture. Addison-
Wesley, 2003. ISBN 0-321-12742-0.
Erich Gamma, Richard Helm, Ralph Johnson & John Vlissides: Design
Patterns: Elements of reusable object-oriented software. Addison-Wesley,
1994. ISBN 0-201-63361-2.
Douglas Schmidt, Michael Stal, Hans Rohnert & Frank Buschmann: Pattern-
Oriented Software Architecture - Volume 2: Patterns for concurrent and
networked objects. John Wiley & Sons, 2000. ISBN 0-471-60695-2.
MDA [Model-Driven Architecture], por ejemplo, es un estándar propuesto por el OMG [Object
Management Group] que promete acelerar el desarrollo de aplicaciones, simplificar la
integración entre distintas tecnologías y reducir el coste de la migración de las aplicaciones a
nuevas plataformas. Ahí es nada.
La idea consiste en desarrollar modelos de alto nivel (como los que siempre deberíamos
construir al diseñar una aplicación). Estos modelos, no obstante, no sólo se utilizan para
generar papel (documentar nuestro diseño), sino que se utilizan para, automáticamente,
transformarlos en artefactos que formarán parte del producto final (esto es, código). Para ello,
los modelos han de ser modelos formales que puedan ser interpretados por un ordenador y,
cuanto más precisos sean, menos trabajo quedará por hacer en la fase de codificación.
En realidad, lo que propone MDA es elevar el nivel de abstracción al que trabajamos y dejar
que sean herramientas las que realicen todas las tareas rutinarias que usualmente se hacen a
mano (por ejemplo, derivar el diseño de la base de datos a partir de un diagrama de clases
que representa un modelo orientado a objetos del dominio de la aplicación).
La clave en el uso con éxito de MDA radica en la utilización de modelos cuya construcción sea
más fácil que la implementación del código correspondiente, de forma que la conversión
automática PIM->PSMs->Código sirva realmente para ahorrarnos trabajo.
Para profundizar...
Anneke Kleppe, Jos Warmer & Wim Bast: MDA Explained - The Model Driven
Architecture: Practice and Promise. Addison-Wesley, 2003. ISBN 0-321-
19442-X