0% encontró este documento útil (0 votos)
601 vistas486 páginas

Curso de C PDF

Cargado por

Rafael Garcia
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
601 vistas486 páginas

Curso de C PDF

Cargado por

Rafael Garcia
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
Está en la página 1/ 486

Curso de C#

La plataforma .NET

Curso on line: http://elvex.ugr.es/decsai/csharp/

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.

CLR [Common Language Runtime]


El Common Language Runtime (CLR) es el núcleo de la plataforma .NET ya que es el
encargado de gestionar la ejecución de código compilado para la plataforma .NET. Puede
asimilarse a la máquina virtual de Java.

Las dos principales características del CLR son:

 Ejecución multiplataforma: El CLR actúa como una máquina virtual,


encargándose de ejecutar las aplicaciones diseñadas para la plataforma
.NET. Su especificación está abierta, por lo que cualquier plataforma para la
que exista una versión del CLR podrá ejecutar cualquier aplicación .NET.
Microsoft ha desarrollado versiones del CLR para la mayoría de las
versiones de Windows. Por otro lado Microsoft ha firmado un acuerdo con
Corel para portar el CLR a Linux y también hay terceros que están
desarrollando de manera independiente versiones de libre distribución del
CLR para Linux. Asimismo, dado que la arquitectura del CLR está
totalmente abierta, es posible que en el futuro se diseñen versiones del
mismo para otros sistemas operativos.
 Integración de lenguajes: Desde cualquier lenguaje para el que exista un
compilador que genere código para la plataforma .NET es posible utilizar
código generado para la misma usando cualquier otro lenguaje tal y como
si de código escrito usando el primero se tratase.

Otras características destacables son:

 Modelo de programación consistente: A todos los servicios y facilidades


ofrecidos por el CLR se accede de la misma forma: a través de un modelo
de programación orientado a objetos.
 Eliminación del "infierno de las DLLs": En la plataforma .NET desaparece
el problema conocido como "infierno de las DLLs" que se da en los sistemas
operativos actuales de la familia Windows ya que en la plataforma .NET las
versiones nuevas de las DLLs pueden coexistir con las viejas.
 Gestión de memoria: El CLR incluye un recolector de basura que evita
que el programador tenga que tener en cuenta cuándo ha de destruir los
objetos que dejen de serle útiles.

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.

 Seguridad de tipos: El CLR facilita la detección de errores de programación


difíciles de localizar comprobando que toda conversión de tipos que se
realice durante la ejecución de una aplicación .NET se haga de modo que
los tipos origen y destino sean compatibles.
 Aislamiento de procesos: El CLR asegura que desde código perteneciente a
un determinado proceso no se pueda acceder a código o datos
pertenecientes a otro, ni se permite acceder a posiciones arbitrarias de
memoria.
 Tratamiento de excepciones: En el CLR todo los errores que se puedan
producir durante la ejecución de una aplicación se propagan de igual
manera: mediante excepciones.
 Soporte multihilo: El CLR es capaz de trabajar con aplicaciones divididas en
múltiples hilos de ejecución que pueden ir evolucionando por separado en
paralelo o intercalándose, según el número de procesadores de la máquina
sobre la que se ejecuten. Las aplicaciones pueden lanzar nuevos hilos,
destruirlos, suspenderlos por un tiempo o hasta que les llegue una
notificación, enviarles notificaciones, sincronizarlos, etc.
 Distribución transparente: El CLR ofrece la infraestructura necesaria para
crear objetos remotos y acceder a ellos de manera completamente
transparente a su localización real, tal y como si se encontrasen en la
máquina que los utiliza.
 Seguridad avanzada: El CLR proporciona mecanismos para restringir la
ejecución de ciertos códigos o los permisos asignados a los mismos según
su procedendecia o el usuario que los ejecute.

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

Constituyen la unidad lógica de despliegue en la plataforma .NET. Un assembly incluye


metadatos acerca de los componentes incluidos en el assembly (versiones, tipos,
dependencias...), metadatos acerca de los tipos incluidos (propiedades, atributos, métodos,
signaturas, clases base...), el código intermedio MSIL (Microsoft Intermediate Language,
similar a los bytecodes de Java) y los recursos adicionales que sean necesarios (imágenes,
textos...). En definitiva, viene a ser algo parecido a los ficheros JAR (Java archives) de Java.

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

Similar a ADO, proporciona acceso a datos de forma independiente al lenguaje de


programación que utilicemos. Los datos se pueden ver y procesar de forma relacional (tablas)
o jerárquica (XML).
Desarrollo de aplicaciones
Formularios Windows

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

 C#: Un nuevo lenguaje creado para la plataforma .NET. Se puede considerar


una versión "segura" de C++ y viene a ser un híbrido entre Java (de Sun) y
Delphi (de Borland). Es un lenguaje de programación orientado a objetos
que pretende facilitar el desarrollo de componentes software robustos y
duraderos que preserven la inversión realizada en su desarrollo (en
palabras de Microsoft).
 Visual Basic .NET: Moderniza y simplifica el lenguaje de programación Visual
Basic, con algunas novedades sintácticas, herencia simple, tratamiento de
hebras y manejo de excepciones.

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:

 Microsoft Windows NT 4.0 (Service Pack 6a)


 Microsoft Windows 2000 (SP 2 recomendado)
 Microsoft Windows XP Professional

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 vs. J2EE [Java 2 Enterprise Edition]

.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:

Visual Studio mostrará la pantalla inicial:


Primeros pasos

Configuración

El entorno de Visual Studio 2005 es completamente configurable, pudiéndose adaptar a las


necesidades y gustos de todo el mundo. La configuración se realiza a través del menú
"Herramientas", y dentro de él, "Personalizar" y "Opciones". Por ejemplo, dentro de "Opciones"
podemos establecer la apariencia de la página de inicio:

Manteniéndose al día...

En la sección "Introducción", podremos acceder a las últimas novedades y noticias


relacionadas con el desarrollo de aplicaciones con VS.NET (siempre que estemos conectados a
Internet, obviamente). Podemos acceder a ejemplos de código, grupos de noticias y
componentes listos para su uso en la plataforma .NET. Incluso podemos buscar información en
la biblioteca de la red de desarrolladores de Microsoft (MSDN Library), descargar las últimas
actualizaciones, etc.

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.

Aprovechando al máximo el espacio disponible

 Cuando tengamos múltiples ficheros abiertos y queramos ver dos simultáneamente,


sólo tendremos que hacer "drag&drop" para que el espacio disponible se reparta
equitativamente ("tiling") entre los documentos que deseemos visualizar. Sólo hay que
pinchar en el tab correspondiente al nombre del documento que queramos ver y llevarlo
hasta el espacio en el que se visualizan los documentos. En función de dónde soltemos el
botón del ratón se creará una división horizontal o vertical en nuestra pantalla. Para
volver al estado inicial sólo tenderemos que arrastrar los documentos de vuelta a donde
estaban.
 Las distintas ventanas visibles en nuestro monitor también pueden arrastrarse para
colocarlas donde más nos interese, como parte de nuestra ventana principal ("dockable")
o como ventanas independientes ("floating"). El estado de una ventana también podemos
cambiarlo si pinchamos con el botón derecho sobre su barra de título.
 Algunas ventanas sólo se utilizan en determinadas situaciones, por lo que podemos
fijar su estado a "auto-hide" para que no ocupen parte de nuestro preciado espacio en
pantalla de forma innecesaria. De hecho, éste es el estado inicial de las ventanas que
aparecen en la parte izquierda de nuestro monitor ("Server Explorer" y "Toolbox"). Es
estado "auto-hide" se puede establecer seleccionando la opción correspondiente del menú
contextual que aparece asociado a las barras de título de las distintas ventana y también
mediante el botoncito con forma de pin que aparece junto al aspa para cerrar la ventana.

Como ejercicio configure su entorno para que tenga este aspecto:


Personalización del entorno

Las opciones de personalización del entorno de desarrollo VS.NET se encuentra centralizadas


en la opción "Opciones" del menú "Herramientas". Las distintas opciones de configuración
están agrupadas por categorías de forma jerárquica. Entre la gran cantidad de opciones
disponibles, son de especial interés las siguientes categorías:

 Entorno: Aspectos generales del entorno, como visualización de documentos y


arranque inicial del entorno ("General"), opciones sobre el manejo de ficheros
("Documentos") tales como uso de ficheros que sean modificados fuera del entorno; tipos
de letra, colores e impresión ("Fuentes y Colores"); sistema de ayuda ("Ayuda"),
configuración del teclado ("Teclado"), etc.
 Control de código fuente: Configuración del entorno para el uso de sistemas de
control de versiones que permitan que varios programadores trabajen concurrentemente
en un proyecto. Principalmente, las opciones disponibles permiten establecer qué acciones
se realizarán automáticamente (vg: check-out de un fichero que se vaya a modificar) y
cuáles requerirán una acción explícita por parte del usuario (vg: check-in de un fichero
una vez que hayamos comprobado que las modificaciones realizadas funcionan
correctamente).
 Editor de texto: Configuración del editor de documentos, en general y en particular
para determinados tipos de archivos (como los escritos en alguno de los lenguajes de
programación soportados por el VS.NET).
 Además de las categorías anteriores, las distintas herramientas instaladas en el
VS.NET incorporan páginas de opciones para su configuración.
La opción "Personalizar" del menú "Herramientas" nos permite modificar las opciones que
aparecen en los menús y barras de botones del VS .NET. Además, cuando el diálogo
"Personalizar" está abierto, podemos modificar los menús y barras de botones del VS.NET
directamente, de una forma mucho más cómoda para nosotros.
Herramientas
Visual Studio .NET incluye una serie de herramientas útiles en el desarrollo de aplicaciones:

Explorador de Soluciones

El explorador de soluciones, nombre que se le da en Visual Studio .NET a un conjunto de


proyectos relacionados, es la herramienta que nos permite gestionar proyectos en VS.NET.
Desde él podemos acceder a los distintos componentes de un proyecto, ver sus propiedades y
ejecutar acciones sobre ellos (p.ej. control de versiones).
Vista de Clases

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

El examinador de objetos nos permite explorar espacios de nombres, clases, estructuras,


interfaces y demás componentes de nuestros proyectos, así como todos aquellos componentes
externos a los que hagan referencia los componentes de nuestro proyecto.
El editor
El editor de textos de VS.NET incluye funciones muy útiles para el programador tales como
búsqueda incremental (con CONTROL+I), "code outlining" (para expandir y contraer bloques
de código), portapapeles múltiple ("clipboard ring", CONTROL+SHIFT+V), selección de
columnas (seleccionando con el ratón mientras se mantiene pulsada la tecla ALT), numeración
de líneas, impresión en color, etc..

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.

#region "Identificador del fragmento de código"


...
#endregion
Macros
Visual Studio.NET viene equipado con un avanzado sistema de macros que nos permiten
personalizar y automatizar las tareas que realicemos de forma repetitiva. El IDE para macros
nos permite desarrollar y depurar macros con las que manipular de forma automática casi
cualquier elemento de Visual Studio .NET (proyectos, editor de código, ventanas de
herramientas, compilación y despliegue de nuestras aplicaciones...).

El submenú 'Macros' de la opción "Herramientas" incluye utilidades como 'Grabar


TemporaryMacro' que graban las acciones que realicemos en el entorno hasta que pulsemos el
botón 'Detener Grabación' (el central en la barra de botones para macros).

Tras finalizar la grabación de la macro temporal, podemos ejecutarla con CONTROL+SHIFT+P.


Además, si es de utilidad para nosotros, podemos guardarla y editarla a nuestro antojo (para
lo cual podemos emplear la herramienta "Explorador de Macros" que aparece en "Ver | Otras
Ventanas | Explorador de Macros").

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#

Introducción al lenguaje de programación


C#

Características del lenguaje C#


Aspectos Léxicos
E/S básica
Tipos de datos
Variables y constantes
Operadores y expresiones
Estructuras de control

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

Características del lenguaje C#


Aunque es pronto para entrar con detenimiento en el lenguaje C# podemos adelantar las
características más relevantes de este lenguaje, características que se describen con
profundidad posteriormente, durante el estudio detallado de los elementos del lenguaje.

 Es autocontenido. Un programa en C# no necesita de ficheros adicionales al


propio código fuente, como los ficheros de cabecera ( .h) de C++, lo que
simplifica la arquitectura de los proyectos software desarrollados con C++.
 Es homogéneo. El tamaño de los tipos de datos básicos es fijo e
independiente del compilador, sistema operativo o máquina en la que se
compile (no ocurre lo que en C++), lo que facilita la portabilidad del código.
 Es actual. C# incorpora en el propio lenguaje elementos que se han
demostrado ser muy útiles para el desarrollo de aplicaciones como el tipo
básico decimal que representa valores decimales con 128 bits, lo que le
hace adecuado para cálculos financieros y monetarios, incorpora la
instrucción foreach, que permite una cómoda iteración por colecciones de
datos, proporciona el tipo básico string, permite definir cómodamente
propiedades (campos de acceso controlado), etc.
 Está orientado a objetos. C# soporta todas las características propias del
paradigma de la programación orientada a objetos: encapsulación, herencia
y polimorfismo.
o Encapsulación: además de los modificadores de accceso
convencionales: public, private y protected, C# añade el
modificador internal, que limita el acceso al proyecto actual.
o C# sólo admite herencia simple.
o Todos los métodos son, por defecto, sellados, y los métodos
redefinibles han de marcarse, obligatoriamente, con el modificador
virtual.
 Delega la gestión de memoria. Como todo lenguaje de .NET, la gestión de
la memoria se realiza automáticamente ya que tiene a su disposición el
recolector de basura del CLR. Esto hace que el programador se desentienda
de la gestión directa de la memoria (petición y liberación explícita) evitando
que se cometan los errores habituales de este tipo de gestión en C++, por
ejemplo.

En principio, en C# todo el código incluye numerosas restricciones para


asegurar su seguridad no permite el uso de punteros, por ejemplo. Sin
embargo, y a diferencia de Java, en C# es posible saltarse dichas
restricciones manipulando objetos a través de punteros. Para ello basta
marcar regiones de código como inseguras (modificador unsafe) y podrán
usarse en ellas punteros de forma similar a cómo se hace en C++, lo que
puede resultar vital para situaciones donde se necesite una eficiencia y
velocidad procesamiento muy grandes.

 Emplea un sistema de tipos unificado. Todos los tipos de datos (incluidos


los definidos por el usuario) siempre derivarán, aunque sea de manera
implícita, de una clase base común llamada System.Object, por lo que
dispondrán de todos los miembros definidos en ésta clase. Esto también es
aplicable, lógicamente, a los tipos de datos básicos.
 Proporciona seguridad con los tipos de datos. C# no admiten ni
funciones ni variables globales sino que todo el código y datos han de
definirse dentro de definiciones de tipos de datos, lo que reduce problemas
por conflictos de nombres y facilita la legibilidad del código.

C# incluye mecanismos que permiten asegurar que los accesos a tipos de


datos siempre se realicen correctamente:

o No pueden usarse variables que no hayan sido inicializadas.


o Sólo se admiten conversiones entre tipos compatibles
o Siempre se comprueba que los índices empleados para acceder a los
elementos de una tabla (vector o matriz) se encuentran en el rango de
valores válidos.
o Siempre se comprueba que los valores que se pasan en una llamada
a métodos que pueden admitir un número indefinido de parámetros
(de un cierto tipo) sean del tipo apropiado.
 Proporciona instrucciones seguras. En C# se han impuesto una serie de
restricciones para usar las instrucciones de control más comunes. Por
ejemplo, toda condición está controlada por una expresión condicional, los
casos de una instrucción condicional múltiple (switch) han de terminar con
una instrucción break o goto, etc.
 Facilita la extensibilidad de los operadores. C# permite redefinir el
significado de la mayoría de los operadores -incluidos los de conversión,
tanto para conversiones implícitas como explícitas- cuando se aplican a
diferentes tipos de objetos.
 Permite incorporar modificadores informativos sobre un tipo o sus
miembros. C# ofrece, a través del concepto de atributos, la posibilidad
de añadir, a los metadatos del módulo resultante de la compilación de
cualquier fuente, información sobre un tipo o sus miembros a la generada
por el compilador que luego podrá ser consultada en tiempo ejecución a
través de la biblioteca de reflexión de .NET. Esto, que más bien es una
característica propia de la plataforma .NET y no de C#, puede usarse como
un mecanismo para definir nuevos modificadores.
 Facilita el mantenimiento (es "versionable"). C# incluye una política de
versionado que permite crear nuevas versiones de tipos sin temor a que la
introducción de nuevos miembros provoquen errores difíciles de detectar en
tipos hijos previamente desarrollados y ya extendidos con miembros de
igual nombre a los recién introducidos.
 Apuesta por la compatibilidad. C# mantiene una sintaxis muy similar a
C++ o Java que permite, bajo ciertas condiciones, incluir directamente en
código escrito en C# fragmentos de código escrito en estos lenguajes.

En resumen, podemos concluir que:

 Es un lenguaje orientado al desarrollo de componentes (módulos


independientes de granularidad mayor que los objetos) ya que los
componentes son objetos que se caracterizan por sus propiedades,
métodos y eventos y estos aspectos de los componentes están presentes
de manera natural en C#.
 En C# todo son objetos: desaparece la distinción entre tipos primitivos y
objetos de lenguajes como Java o C++ (sin penalizar la eficiencia como en
LISP o Smalltalk).
 El software es robusto y duradero: el mecanismo automático de recolección
de basura, la gestión de excepciones, la comprobación de tipos, la
imposibilidad de usar variables sin inicializar y hacer conversiones de tipo
(castings) no seguras, gestión de versiones, etc. ayudan a desarrollar
software fácilmente mantenible y poco propenso a errores.
 Además, no hay que olvidar el aspecto económico: la posibilidad de utilizar
C++ puro (código no gestionado o inseguro), la facilidad de
interoperabilidad (XML, SOAP, COM, DLLs...) junto con un aprendizaje
relativamente sencillo (para los que ya conocen otros lenguajes de
programación) hace que el dominio y uso del lenguaje junto a otras
tecnologías sea muy apreciado.
Aspectos Léxicos
En esta sección presentaremos las reglas sintácticas básicas que deben cumplir los programas
escritos en C# y veremos los elementos fundamentales de cualquier programa en C#
(identificadores, comentarios, palabras reservadas, etc.). Se trata, en definitiva, de la parte
instrumental y básica, además de imprescindible, de cualquier manual de programación.

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;

public class SaludoAlMundo


{
public static void Main( )
{
// Mostrar en la consola el mensaje: ¡Hola, mundo!

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 */.

C# es un lenguaje orientado a objetos y todo está encapsulado en clases, incluso la función


Main que es un método (especial) del objeto aplicación. En el ejemplo la clase se llama
SaludoAlMundo.

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.

Cuando este programa se compile y se proceda a su ejecución, la primera función


(estrictamente hablando, método) en ejecutarse será Main. Los programadores de C++
deberán tener especial cuidado y no confundirlo con main().

La función Main() tiene los siguientes prototipos válidos:

 Si la función no devuelve ningún valor, puede usarse:


 public static void Main( )
 public static void Main(string [] args)

La diferencia está en la posibilidad de suministrar argumentos en la llamada


a la ejecución del programa. Main() procesan los argumentos tomándolos
de la lista de cadenas args (más adelante se detalla cómo).

 Si la función devuelve un valor al proceso que invoca la ejecución del


programa, el valor debe ser entero (int) y, como en el caso anterior,
puede usarse:
 public static int Main( )
 public static int Main(string [] args)

La convención es que el valor 0 indica que el programa termina


correctamente.

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.

A continuacón detallaremos ciertos aspectos léxicos importantes de los programas escritos en


C#, algunos de los cuales ya se han comentado brevemente como explicación al programa
introductorio.

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:

 Comentarios de línea. Están precedidos de la construcción // y su efecto


(ámbito) termina en la línea en la que está inmerso.
 Comentarios de formato libre. Están delimitados por las construcciones /* y
*/ y pueden extenderse por varias líneas.
Ejemplos de comentarios

// En una línea, al estilo de C++

/*
En múltiples líneas, como se viene
haciendo desde "los tiempos de C"
*/

/* Este tipo de comentario ya no es habitual */

Identificadores

Un identificador es un nombre con el que nos referimos a algún elemento de nuestro


programa: una clase, un objeto, una variable, un método, etc. Se imponen algunas
restricciones acerca de los nombres que pueden emplearse:

 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).

Los identificadores con prefijo @ se conocen como identificadores literales.


Aunque el uso del prefijo @ para los identificadores que no son palabras
clave está permitido, no se recomienda por regla de estilo.

Palabras reservadas

Las palabras reservadas son identificadores predefinidos reservados que tienen un


significado especial para el compilador por lo que no se pueden utilizar como identificadores en
un programa a menos que incluyan el carácter @ como prefijo.

Las palabras reservadas en C# son, por orden alfabético:

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).

Los literales pueden ser:


 Literales lógicos.

Existen dos valores literales lógicos: true y false. El tipo de un literal


lógico es bool.

 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.

El tipo de un literal entero se determina como sigue:

o Si no tiene sufijo, su tipo es int.


o Si tiene el sufijo U o u, su tipo es uint o ulong.
o Si tiene el sufijo L o l, su tipo es long o ulong.
o Si tiene el sufijo UL, Ul, uL, ul, LU, Lu, lU o lu es de tipo ulong.

A la hora de escribir literales de tipo long se recomienda usar L en lugar


de l para evitar confundir la letra l con el dígito 1.

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.

El tipo de un literal real se determina como sigue:

o Si no se especifica sufijo, el tipo es double.


o Si el sufijo es F o f es de tipo float.
o Si el sufijo es D o d es de tipo double.
o Si el sufijo es M o m es de tipo 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

1f, 1.5f, 1e10f, 123.456F, 123f y 1.23e2f // float


1d, 1.5d, 1e10d, 123.456D, 123.0 y 123D // double
1m, 1.5m, 1e10m, 123.456M, 123.456m y 12.3E1M // decimal.

 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'.

Una secuencia de escape sencilla representa una codificación de caracteres


Unicode y está formada por el carácter \ seguido de otro carácter. Las
secuencias válidas se describe en la siguiente tabla.

Secuencia de Nombre del Codificación


escape carácter Unicode
\' Comilla simple 0x0027
\" Comilla doble 0x0022
\\ Barra invertida 0x005C
\0 Null 0x0000
\a Alerta 0x0007
\b Retroceso 0x0008
\f Avance de página 0x000C
\n Nueva línea 0x000A
\r Retorno de carro 0x000D
\t Tabulación horizontal 0x0009
\v Tabulación vertical 0x000B

El tipo de un literal de caracteres es char.

Literales de caracteres

'A' // caracter sencillo


'\u0041' // caracter Unicode
'\x0041' // unsigned short hexadecimal
'\n' // caracter de escape: CR+LF

 Literales de cadena.

C# admite dos formatos de literales de cadena: literales de cadena típicos y


literales de cadena textuales. El tipo de un literal de cadena es string.

Un literal típico de cadena consta de cero o más caracteres entre comillas


dobles y puede incluir secuencias de escape sencillas y secuencias de
escape hexadecimales y Unicode.

Literales tipicos de cadena

"!Hola, mundo!" // !Hola, mundo!


"!Hola, \t mundo!" // !Hola, mundo!
"" // La cadena vacia

Un literal de cadena textual consta del carácter @ seguido de un carácter de


comillas dobles, cero o más caracteres y un carácter de comillas dobles de
cierre. Por ejemplo, @"Hola". En estos literales los caracteres se
interpretan de manera literal y no se procesan las secuencias de escape,
con la única excepción de la secuencia \". Un literal de cadena textual
puede estar en varias líneas.

Literales de cadena textuales

@"!Hola, \t mundo!" // !Hola, \t mundo!


"Me dijo \"Hola\" y me asustó" // Me dijo "Hola" y me
asustó
@"Me dijo ""Hola"" y me asustó" // Me dijo "Hola" y me
asustó
"\\\\servidor\\share\\file.txt" //
\\servidor\share\file.txt
@"\\servidor\share\file.txt" //
\\servidor\share\file.txt
@"uno // Esta es una cadena
distribuida
dos" // en dos lineas.

 Literal null.

Su único valor es null y su tipo es el tipo null.

Órdenes

 Delimitadas por punto y coma (;) como en C, C++ y Java.


 Los bloques { ... } no necesitan punto y coma al final.
E/S básica
Las operaciones de entrada y salida tienen como objetivo permitir que el usuario pueda
introducir información al programa (operaciones de entrada) y que pueda obtener información
de éste (operaciones de salida). En definitiva, tratan de la comunicación entre el usuario y el
programa.

La manera más simple de comunicación es mediante la consola. La consola ha sido el modo


tradicional de comunicación entre los programas y el usuario por su simplicidad. Las
aplicaciones basadas en ventanas resultan mucho más atractivas y cómodas para el usuario y
es éste, sin duda, el tipo de comunicación que deberemos emplear para productos finales. Los
programas que no requieran una mucha interacción con el usuario, no obstante, se construyen
y se ponen en explotación mucho más rápidamente si se utiliza la consola como medio de
comunicación.

Aplicaciones en modo consola

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:

El acceso a la consola lo facilita la clase Console, declarada en el espacio de nombres


System. Esa clase proporciona la compatibilidad básica para aplicaciones que leen y escriben
caracteres en la consola. No es necesario realizar ninguna acción para poder obtener datos de
la consola a partir de la entrada estándar (teclado) o presentarlos en la salida estándar
(consola) ya que estos flujos (junto con el del error estándar) se asocian a la consola de
manera automática, como ocurre en C++, por ejemplo, con cin, cout y cerr.
Los métodos básicos de la clase Console son WriteLine y ReadLine, junto con sus
variantes Write y Read:

 WriteLine escribe una línea en la salida estándar, entendiendo que escribe


el terminador de línea actual (por defecto la cadena "\r\n").

La versión más simple de este método recibe un único argumento (una


cadena) cuyo valor es el que se muestra:

Console.WriteLine ("!Hola, " + "mundo!");


// Escribe: !Hola, mundo!

Otra versión permite mostrar variables de diferentes tipos (sin necesidad de


convertirlas a string. La llamada tiene un número indeterminado de
argumentos: el primero es una cadena de formato en la que las variables a
mostrar se indican con {0}, {1}, etc. y a continuación se enumeran las
variables a mostrar, entendiendo que la primera se "casa" con {0}, la
segunda con {1}, etc. Esta manera de mostrar los datos recuerda a la
instrucción printf de C, que cayó en desuso con C++ ...

int TuEdad = 25;


string TuNombre = "Pepe";
Console.WriteLine ("Tienes {0} años, {1}.", TuEdad,
TuNombre);
// Escribe: Tienes 25 años, Pepe.

 El método Write hace lo mismo que WriteLine aunque no escribe el


terminador de línea.
 ReadLine lee la siguiente línea de caracteres de la secuencia de entrada
estándar (el teclado, por defecto), eliminando del buffer de entrada el
terminador de línea. Devuelve la cadena leida, que no contiene el carácter
o los caracteres de terminación.
 Read lee el siguiente carácter de la secuencia de entrada estándar y devuelve
un valor de tipo int. La lectura se realiza del buffer de entrada y no se
termina (no devuelve ningún valor) hasta que se encuentra al caracter de
terminación (cuando el usuario presionó la tecla ENTER). Si existen datos
disponibles en el buffer, la secuencia de entrada contiene los datos
introducidos por el usuario, seguidos del carácter de terminación.

Veamos un sencillo ejemplo sobre el uso de estos métodos.

E/S simple

using System;

class PideNombre
{
static void Main(string[] args)
{
Console.Write ("Introduzca su nombre: "); // 1
string nombre = Console.ReadLine(); // 2

Console.WriteLine ("Su nombre es: " + nombre); // 3


Console.ReadLine(); // 4
}
}
La instrucción 1 muestra la cadena Introduzca su nombre:

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:

MessageBox.Show ("¡Hola, mundo!", "Un saludo típico");


(en realidad, System.Windows.Forms.MessageBox.Show (...);)

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.

C# proporciona seguridad con los tipos de datos. C# no admiten ni funciones ni variables


globales sino que todo el código y datos han de definirse dentro de definiciones de tipos de
datos, lo que reduce problemas por conflictos de nombres y facilita la legibilidad del código. C#
incluye mecanismos que permiten asegurar que los accesos a tipos de datos siempre se
realicen correctamente:

 No pueden usarse variables que no hayan sido iniciadas.


 El tipo asignado restringe los valores que puede almacenar y las operaciones
en las que puede intervenir.
 Siempre se comprueba que los índices empleados para acceder a los
elementos de una tabla (vector o matriz) se encuentran en el rango de
valores válidos.
 Sólo se admiten conversiones de tipo entre tipos compatibles y entre aquellos
que se hayan definido explícitamente el mecanismo de conversión (En C#
puede implementarse la manera en que se realiza la conversión implícita y
explícita entre tipos)

Los tipos de datos en C# pueden clasificarse en dos grandes categorías:

 tipos valor
 tipos referencia

y pueden caracterizarse como sigue:


Tipos valor Tipos referencia
La variable contiene un valor La variable contiene una referencia
El dato se almacena en la pila El dato se almacena en el heap
El dato siempre tiene valor El dato puede no tener valor null
Una asignación copia el valor Una asignación copia la referencia

int i = 123; // tipo valor

string s = "Hello world"; // tipo referencia

El comportamiento cuando se copian o modifican objetos de estos tipos es muy diferente.

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.

Nombre (.NET Tamaño


Alias Descripción Rango
Framework) (bytes)
Bytes con
System.Sbyte sbyte 1 -128 ... 127
signo
Enteros
System.Int16 short 2 -32.768 ... 32.767
cortos
-2.147.483.648 ...
System.Int32 int Enteros 4
2.147.483.647
-9.223.372.036.854.775.808
Enteros
System.Int64 long 8 ...
largos
9.223.372.036.854.775.807
Bytes (sin
System.Byte byte 1 0 ... 255
signo)
Enteros
System.Uint16 ushort cortos (sin 2 0 ... 65.535
signo)
Enteros (sin 0 ...
System.UInt32 uint 4
signo) 18.446.744.073.709.551.615
Enteros
0 ...
System.Uint64 ulong largos (sin 8
18.446.744.073.709.551.615
signo)
Reales (7 ±1.5 x 10-45 ... ±3.4 x
System.Single float 4
decimales) 10+38
Reales (15-
±5.0 x 10-324 ... ±1.7 x
System.Double double 16 8
10+308
decimales)
Reales (28-
±1.0 x 10-28 ... ±7.9 x
System.Decimal decimal 29 12
10+28
decimales)
Caracteres
System.Char char 2 Cualquier carácter Unicode
Unicode
Valores
System.Boolean bool 1 true ó false
lógicos

El comportamiento de los datos de tipos valor es el esperado cuando se inician o reciben un


valor por asignación a partir de otro dato de tipo valor (son independientes).

Tipos referencia

Si un dato es de un tipo referencia contiene la dirección de la información, en definitiva, una


referencia al objeto que contiene los datos y/o métodos. En definitiva, distinguimos:

 La referencia o un nombre por el que nos referimos al objeto y que utilizamos


para manipularlo.
 El objeto referenciado, que ocupa lugar en memoria (en el heap) y que
almacenará el valor efectivo del objeto.
En definitiva: la variable y su contenido "lógico" están en posiciones de memoria diferentes. El
valor almacenado en una variable de tipo referencia es la dirección de memoria del objeto
referenciado (es una referencia) o tiene el valor null (no referencia a nada). Observe que
pueden existir dos variables que referencien al mismo objeto (pueden existir dos referencias a
la misma zona de memoria).

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++:

 En C++ indica que se pide memoria dinámica.


 En C# indica que se llama al constructor de una clase.

El efecto, no obstante, es similar ya que como la variable es de un tipo referencia, al llamar al


constructor se aloja memoria en el heap de manera implícita.

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
{

static void Main(string[] args)


{
ObjetoDemo o1 = new ObjetoDemo(); // new llama a un
constructor
o1.Valor = 10;
ObjetoDemo o2 = new ObjetoDemo(); // new llama a un
constructor
o2 = o1; // La memoria que ocupaba el objeto
refernciado por "o2"
// se pierde: actuará el recolector de
basura.
PintaDatos ("o1", "o2", o1, o2);

ObjetoDemo o3 = new ObjetoDemo();// new llama a un


constructor
o3.Valor = 10;
ObjetoDemo o4 = o3; // "o4" contiene la misma
direccion de memoria que "o3"
o4.Valor = 20; // Igual que hacer o3.Valor = 20;
PintaDatos ("o3", "o4", o3, o4);
ObjetoDemo o5 = new ObjetoDemo(); // new llama a un
constructor
o5.Valor = 10;
ObjetoDemo o6 = new ObjetoDemo(); // new llama a un
constructor
o6.Valor = o5.Valor;
PintaDatos ("o5", "o6", o5, o6);

Console.ReadLine();
}

static void PintaDatos (string st1, string st2,


ObjetoDemo ob1, ObjetoDemo ob2)
{
Console.Write ("{0} = {1}, {2} = {3} ", st1,
ob1.Valor, st2, ob2.Valor);
if (ob1==ob2)
Console.WriteLine ("{0} == {1}", st1, st2);
else
Console.WriteLine ("{0} != {1}", st1, st2);
}
}

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

Una variable permite el almacenamiento de datos en la memoria. Es una abstracción que


permite referirnos a una zona de memoria mediante un nombre (su identificador). Todas las
variables tienen asociadas un tipo que determina los valores que pueden almacenarse y las
operaciones que pueden efectuarse con los datos de ese tipo. Además, el término variable
indica que el contenido de esa zona de memoria puede modificarse durante la ejecución del
programa.

Nombres de variables

Los nombres que pueden asignarse a las variables deben regirse por unas normas básicas:

 Pueden contener letras, dígitos y el caracter de subrayado (_).


 No pueden empezar con un número: deben comenzar por una letra letra o
con el carácter de subrayado (_).

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:

 Darle un nombre significativo: Area


 Asociarle un tipo: dado que puede tener decimales y no se requiere una gran
precisión, bastará con el tipo float.

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;

pueden simplificarse en una línea:

float Radio, Area;


De la misma manera pueden declararse e inicializarse variables en una sola línea:

int a=1, b=2, c, d=4;

No existe ninguna zona predeterminada en el código para la declaración de variables, la única


restricción es que la declaración debe realizarse antes de su uso.

No es conveniente abusar de la declaración múltiple de variables en una línea. Desde el punto


de vista de la legibilidad es preferible, por regla general, que cada variable se declare
separadamente y que la declaración vaya acompañada de un breve comentario:

float Radio; // Radio del circulo del cual se calcula el


area.
float Area; // Area del circulo

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.

En la misma línea de la declaración puede asignarse un valor a la variable, por lo que


declaración e inicialización:

float Radio; // Declaracion


Radio = 10.0F; // Inicializacion

pueden simplificarse en una sola línea:

float Radio = 10.0F; // Declaracion e Inicializacion

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:

Area = 2 * 3.1416F * Radio * Radio;

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.

Leer el valor de una variable no modifica el contenido de la variable.

A modo de resumen, un programa que calcula y muestra el área de un círulo de radio 10 es el


siguiente:

Calculo del área de un círculo (1)

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.

Sintácticamente se especifica que un dato es constante al preceder con la palabra reservada


const su declaración. Por ejemplo, para declarar un a constante de tipo float llamada PI y
asignarle el valor (constante) 3.1416 se escribirá:

const float PI = 3.1416F;

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.

Podemos modificar el programa anterior para que utilice la constante PI:

Calculo del área de un círculo (2)

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();
}
}

Ámbito de variables y constantes

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 C# el ámbito abarca desde el lugar de su declaración hasta donde termina el bloque en el


que fue declarada.

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++):

static void Main(string[] args)


{
float Radio = 10.0F;
...
if (Radio > 0){
float Radio = 20.0F; // Error: No se puede
declarar una variable
// local denominada 'Radio'
en este ámbito.
}
...
}
Operadores y expresiones
Un operador está formado por uno o más caracteres y permite realizar una determinada
operación entre uno o más datos y produce un resultado. Es una manera simbólica de expresar
una operación sobre unos operandos.

C# proporciona un conjunto fijo, suficiente y completo de operadores. El significado de cada


operador está perfectamente definido para los tipos predefinidos, aunque algunos de ellos
pueden sobrecargarse, es decir, cambiar su significado al aplicarlos a un tipo definido por el
usuario.

C# dispone de operadores aritméticos, lógicos, relacionales, de manipulación de bits,


asignación, para acceso a tablas y objetos, etc. Los operadores pueden presentarse por
diferentes criterios, por ejemplo, por su funcionalidad:

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

 Los operadores aritméticos de C# son los que se emplean comúnmente en


otros lenguajes: + (suma), - (resta), * (multiplicación), / (división) y %
(módulo o resto de la divisió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).

 Los operadores lógicos proporcionan un resultado de tipo lógico ( bool) y


los operadores a nivel de bit actúan sobre la representación interna de
sus operandos (de tipos enteros) y proporcionan resultados de tipo
numérico.
Los operadores binarios son: & (operación Y lógica entre argumentos
lógicos u operación Y bit a bit entre operandos numéricos), | (operación O,
lógica ó bit a bit, dependiendo del tipo de los argumentos) , ^ (O exclusivo,
lógico ó bit a bit), && (Y lógico, que evalúa el segundo operando solo
cuando es necesario) y || (O lógico, que evalúa el segundo operando solo
cuando es necesario).

Los operadores unarios son: ! (negación o complemento de un argumento


lógico) y ~ (complemento bit a bit de un argumento numérico).

 El operador + para la concatenación de cadenas es un operador binario.


Cuando al menos uno de los operandos es de tipo string este operador
actúa uniendo las representaciones de tipo string de los operandos.
 Operadores de incremento y decremento. El operador de incremento (++)
incrementa su operando en 1 mientras que el de decremento ( --)
decrementa su operando en 1. Puede aparecer antes de su operando: ++v
(incremento prefijo) o después: v++ (incremento postfijo).

El incremento prefijo hace que el resultado sea el valor del operando


después de haber sido incrementado y el postfijo hace que el resultado sea
valor del operando antes de haber sido incrementado.

Los tipos numéricos y de enumeración poseen operadores de incremento y


decremento predefinidos.

 Los operadores de desplazamiento son operadores binarios. Producen un


desplazamiento a nivel de bits de la representación interna del primer
operando (de un tipo entero), a la izquierda (<<) o a la derecha (>>) el
número de bits especificado por su segundo operando.
 Los operadores relacionales proporcionan un resultado lógico, dependiendo
de si sus argumentos son iguales (==), diferentes (!=), o del orden relativo
entre ellos (<, >, <= y >=).
 La asignación simple (=) almacena el valor del operando situado a su
derecha en una variable (posición de memoria) indicada por el operando
situado a su izquierda.

Los operandos deben ser del mismo tipo o el operando de la derecha se


debe poder convertir implícitamente al tipo del operando de la izquierda).

El operador de asignación = produce los siguientes resultados:

o En tipos simples el funcionamiento es similar al de C++, copia el


contenido de la expresión de la derecha en el objeto que recibe el
valor.
o En datos struct realiza una copia directa del contenido, como en
C++.
o En clases se copia la referencia, esto es, la dirección del objeto,
provocando que el objeto sea referenciado por más de una referencia.

Este comportamiento es distinto al que se produce en C++, en el que


se copia el contenido del objeto. Si el objeto contiene estructuras más
complejas, C++ requiere normalmente la sobrecarga del operador
para personalizar la manera en que se realiza la copia para que cada
instancia de la clase (fuente y destino) maneje su propia zona de
memoria. En C# no se permite la sobrecarga del operador =
Los otros operadores de esta categoría realizan, además de la asignación
otra operación previa a la asignación. Por ejemplo,

a += 22;

equivale a

a = a + 22;

o sea, primero se calcula la expresión a+22 y posteriormente,ese valor se


almacena en a.

De la misma manera actúan los demás operadores de asignación: -=, *=,


/=, %=, &=, |=, ^=, <<= y >>=.

Para asignar instancias de clases (en el sentido clásico de C++) se debe


redefinir el método MemberwiseCopy() que es heredado por todas las
clases desde System.Object (todas las clases heredan, en última instancia
de System.Object).

 El operador de acceso a miembros es el operador punto (.) y se emplea


para acceder a los miembros (componentes) de una clase, estructura o
espacio de nombres.
 El operador de acceso por índices es el tradicional, formado por la pareja de
caracteres [ y ]. En C# también se emplea para especificar atributos.
 El operador de conversión explícita de tipos (casting) es el clásico,
formado por la pareja de caracteres ( y ).
 El operador ternario condicional evalúa una condición lógica y devuelve uno
de dos valores.

Se utiliza en expresiones de la forma:

cond ? expr1 : expr2

de manera que si cond es verdad, se evalúa expr1 y se devuelve como


resultado; si cond es falsa se evalúa expr1 y se devuelve como resultado.

 Creación de objetos (new). El operador new permite crear instancias de una


clase definida por el usuario. Por ejemplo: ObjetoDemo o1 = new
ObjetoDemo() declara una referencia (llamada o1) a un objeto de la clase
ObjetoDemo y crea una instancia de esa clase, referenciada por o1. En
realidad, la creación de un objeto implica la llamada a un constructor de la
clase.
 Control de excepciones de desbordamiento: checked y unchecked

C# proporciona la posibilidad de realizar operaciones de moldeado (casting)


y aritméticas en un contexto verificado. En otras palabras, el entorno de
ejecución .NET detecta cualquier situación de desbordamiento y lanza una
excepción (OverFlowException) si ésta se manifiesta.

En un contexto verificado (checked), si una expresión produce un valor


fuera del rango del tipo de destino, el resultado depende de si la expresión
es constante o no. Las expresiones constantes producen errores de
compilación, mientras que las expresiones no constantes se evalúan en
tiempo de ejecución y producen excepciones.

Si no se especifican ni checked ni unchecked, las expresiones constantes


utilizan la verificación de desbordamiento predeterminada en tiempo de
compilación, que es checked. De lo contrario, si la expresión no es
constante, la verificación de desbordamiento en tiempo de ejecución
depende de otros factores tales como las opciones del compilador y la
configuración del entorno. En el siguiente ejemplo, el valor por defecto es
unchecked:

using System;

class PruebaDesbordamiento
{
static short x = 32767; // Maximo valor short
static short y = 32767;

public static int UnCh()


{
int z = unchecked((short)(x + y));
return z; // -2
}
public static int UnCh2()
{
int z = (short)(x + y); // Por defecto es unchecked
return z; // -2
}
public static void Main()
{
Console.WriteLine("Valor -unchecked-: {0}", UnCh());
Console.WriteLine("Valor por defecto: {0}", UnCh2());
Console.ReadLine();
}
}

El resultado es:

Valor -unchecked-: -2
Valor por defecto: -2

Si añadimos la función:

public static int Ch()


{
int z = checked((short)(x + y));
return z;
}

y la llamada:

Console.WriteLine("Valor -checked- : {0}", Ch());


la ejecución del programa provoca el lanzamiento de una excepción
System.OverflowException que detiene la ejecución del programa, al no
estar controlada.
Operadores especiales: as is typeof

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

donde <expresion> es una expresión de un tipo de referencia, y type es un tipo de


referencia. Si la conversión de tipo no es posible, el resultado es null.

// Ejempo de uso del operador as

using System;

class Clase1 {}

class Clase2 {}

public class Demo


{
public static void Main()
{
object [] Objetos = new object[6];
Objetos[0] = new Clase1();
Objetos[1] = new Clase1();
Objetos[2] = "Hola";
Objetos[3] = 123;
Objetos[4] = 123.4;
Objetos[5] = null;

for (int i=0; i<Objetos.Length; ++i)


{
string s = Objetos[i] as string;
Console.Write ("{0}:", i);
if (s != null)
Console.WriteLine ( "'" + s + "'" );
else
Console.WriteLine ( "no es string" );
}
Console.ReadLine();
}
}

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

En el siguiente ejemplo, la función DoSomething recibe cualquier argumento, pero sólo


cuando es de tipo Car se ejecuta el método Drive sobre el objeto c (de clase Car).
static void DoSomething(object o) {
Car c = o as Car;

if (c != null) c.Drive();
}

El operador is

El operador is se utiliza para comprobar en tiempo de ejecución si el tipo de un objeto es


compatible con un tipo dado. El operador is se utiliza en expresiones de la forma:

<expresion> is type

donde <expresion> es una expresión de un tipo de referencia, y type es un tipo de


referencia.

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.

static void DoSomething(object o)


{
if (o is Car) ((Car)o).Drive();
}

El operador typeof

El operador typeof devuelve el objeto derivado de System.Type correspondiente al tipo


especificado.

Una expresión typeof se presenta de la siguiente forma:

typeof (tipo)

donde tipo es el tipo cuyo objeto System.Type se desea obtener.

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:

// Ejempo de uso de los operadores is, as y typeof

using System;

class Clase1 {}

class Clase2 {}

public class Demo


{
public static void Prueba (object o)
{
Clase1 a;
Clase2 b;

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);
}

public static void Main()


{
Clase1 c1 = new Clase1();
Clase2 c2 = new Clase2();
Prueba (c1);
Prueba (c2);
Prueba ("Un string");

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)

Acceso a miembros: x.y

Llamada a métodos: f(x)

Acceso con índices: a[x]

Post-incremento: x++

Primarios Post-decremento: x--

Llamada a un constructor: new

Consulta de tipo: typeof

Control de desbordamiento activo: checked

Control de desbordamiento inactivo: unchecked

Valor positivo: +

Valor negative: -

No: !

Unarios Complemento a nivel de bit: ~

Pre-incremento: ++x

Post-decremento: --x

Conversión de tipo -cast-: (T)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: >

Menor o igual: <=


Relacionales
Mayor o igual: >=

Igualdad o compatibilidad de tipo: is

Conversión de tipo: as

Igualdad ==
Desigualdad !=
Bitwise AND &
Bitwise XOR ^
Bitwise OR |
Logical AND &&
Logical OR ||
Condicional ternario ?:
Asignación =, *=, /=, %=, +=, -=, <<=, >>=, &=, ^=, |=

Asociatividad

Como siempre, es mejor utilizar paréntesis para controlar el orden de evaluación

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.

A modo de resumen, las características propias de las estructuras de control de C# son:

 goto no puede saltar dentro de un bloque (da igual, de todas formas no lo


usaremos NUNCA).
 switch funciona como en Pascal (no como en C).
 Se añade una nueva estructura de control iterativa: foreach.

Estructuras condicionales

if, if-else

La estructura condicional tiene la sintaxis clásica, con la diferencia indicada anteriormente


acerca del tipo de la expresión. Si debe ejecutar más de una instrucción, se encierran en un
bloque, delimitado por las llaves { y }.

Si sólo se actúa cuando la condición es cierta:

if (a > 0) if (a > 0) {
Console.WriteLine ("Positivo");
Console.WriteLine ("Positivo");

ContPositivos++;
}

Si se actúa cuando la condición es falsa se emplea la palabra reservada else:

if (a > 0) if (a > 0) {
Console.WriteLine ("Positivo");
Console.WriteLine ("Positivo");
else
ContPositivos++;
Console.WriteLine ("No Positivo"); }
else {
Console.WriteLine ("No Positivo");

ContNoPositivos++;
}

Puede escribirse una instrucción condicional dentro de otra instrucción condicional,


lógicamente:

if (a > 0) {
Console.WriteLine ("Positivo");
ContPositivos++;
}
else {
if (a < 0) {
Console.WriteLine ("Negativo");
ContNegativos++;
}
else {
Console.WriteLine ("Cero");
ContCeros++;
}
}

La concordancia entre if y else se establece de manera sencilla: cada else se asocia al


último if que no tenga asociado un bloque else.

switch

La estructura de selección múltiple switch funciona sobre cualquier tipo predefinido


(incluyendo string) o enumerado (enum) y debe indicar explícitamente cómo terminar cada
caso (generalmente, con break en situaciones "normales" ó throw en situaciones
"anormales", aunque es posible -pero no recomendable- emplear goto case ó return ):

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.

Un ejemplo que usa un datos string para controlar la selección:

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;
}

static void Main(string[] args)


{
Console.WriteLine (Test("")); // 0
Console.WriteLine (Test("A")); // 2
Console.WriteLine (Test("B")); // 1
Console.WriteLine (Test("C")); // 1
Console.WriteLine (Test("?")); // 0
Console.ReadLine();
}
}
}
Estructuras repetitivas

Las estructuras repetitivas de C# (while, do...while, for) no presentan grandes


diferencias respecto a las de otros lenguajes, como C++. La aportación fundamental es la de la
estructura iterativa en colecciones foreach.

while

int i = 0;

while (i < 5) {
...
i++;
}

do...while

int i = 0;

do {
...
i++;
} while (i < 5);

for

int i;

for (i=0; i < 5; 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):

public static void Main(string[] args)


{
foreach (string s in args)
Console.WriteLine(s);
}

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 ejemplo siguiente realiza la misma función:


public static void Main(string[] args)
{
for (int i=0; i < args.Lenght; i++)
Console.WriteLine(args[i]);
}

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):

foreach (int i in v) for (int i=0; i < v.Length;


i++)
i *= 2; v[i] *= 2;

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;
}

static void Main(string[] args)


{
int [,] coleccion = new int [2,3] {{1,0,4},{3,2,5}};
int f,c;

int valor = Convert.ToInt32(args[0]);

Busca (valor, coleccion, out f, out c);


Console.WriteLine ("El valor {0} esta en [{1},{2}]", valor,f,c);
Console.ReadLine();
}
}
}
break

Lo usaremos únicamente en sentencias switch.

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.

Como cualquier lenguaje de programación, C# proporciona una serie de tipos predefinidos


(int, byte, char, string, ...) y mecanismos para que el usuario cree sus propios tipos
(class y struct).

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.

Los tipos predefinidos en C# están definidos en el espacio de nombres System, que es el


espacio de nombres más numeroso (e importante) de la plataforma .NET. Por ejemplo, para
representar números enteros de 32 bits con signo se utiliza el tipo de dato System.Int32 y a
la hora de crear un objeto a de este tipo que represente el valor 2 se usa la siguiente sintaxis:

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;

System.Int32 no es el único tipo de dato básico incluido en C#. En el espacio de nombres


System se han incluido los siguientes tipos:

C# Tipo en System Características Símbolo


sbyte System.Sbyte entero, 1 byte con signo
byte System.Byte entero, 1 byte sin signo
short System.Short entero, 2 bytes con signo
ushort System.UShort entero, 2 bytes sin signo
int System.Int32 entero, 4 bytes con signo
uint System.UInt32 entero, 4 bytes sin signo U
long System.Int64 entero, 8 bytes con signo L
ulong System.ULong64 entero, 8 bytes sin signo UL
float System.Single real, IEEE 754, 32 bits F
double System.Double real, IEEE 754, 64 bits D
real, 128 bits (28 dígitos
decimal System.Decimal M
significativos)
bool System.Boolean (Verdad/Falso) 1 byte
char System.Char Carácter Unicode, 2 bytes ´´
string System.String Cadenas de caracteres Unicode ""
Cualquier objeto (ningún tipo
object System.Object
concreto)

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++.

Algunos métodos estáticos y propiedades particularmente interesantes son:

 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

Una conversión de tipo (casting) puede ser implícita o explícita.

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.

Por ejemplo, dada la siguiente declaración:

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.

En el último ejemplo, si cada vez


que asignamos un valor a o ejecutamos
Console.WriteLine (o.ToString()) o simplemente Console.WriteLine (o);
obtendremos este resultado:

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.

En el último ejemplo, si cada vez que asignamos un valor a o ejecutamos


Console.WriteLine (o.getType()) obtendremos este resultado:

System.Int32
System.String
System.Double
System.Int32[]
System.Boolean

 protected object MemberwiseClone() Crea una copia superficial del objeto


Object actual. No se puede reemplazar este método; una clase derivada debe
implementar la interfaz ICloneable si una copia superficial no es apropiada.
MemberwiseClone() está protegido y, por tanto, sólo es accesible a través de esta clase
o de una clase derivada. Una copia superficial crea una nueva instancia del mismo tipo
que el objeto original y, después, copia los campos no estáticos del objeto original. Si el
campo es un tipo de valor, se realiza una copia bit a bit del campo. Si el campo es un tipo
de referencia, la referencia se copia, pero no se copia el objeto al que se hace referencia;
por lo tanto, la referencia del objeto original y la referencia del punto del duplicado
apuntan al mismo objeto. Por el contrario, una copia profunda de un objeto duplica todo
aquello a lo que hacen referencia los campos del objeto directa o indirectamente.

Polimorfismo -boxing y unboxing-

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.

Por ejemplo, considere la siguiente declaración de una variable de tipo de valor:

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:

En definitiva, el efecto del boxing es el de cualquier otro tipo de casting pero:

 el contenido de la variable se copia al heap


 se crea una referencia a ésta copia

Aunque no es necesario, también es posible realizar el boxing explícitamente como en el


siguiente ejemplo:

int i = 123;
object o = (object) i;

El siguiente ejemplo convierte una variable entera i a un objeto o mediante boxing. A


continuación, el valor almacenado en la variable i se cambia de 123 a 456. El ejemplo
muestra que el objeto mantiene la copia original del contenido, 123.

// Boxing de una variable int


using System;
class TestBoxing
{
public static void Main()
{
int i = 123;
object o = i; // boxing implicito
i = 456; // Modifica el valor de i
Console.WriteLine("Valor (tipo valor) = {0}", i);
Console.WriteLine("Valor (tipo object)= {0}", o);
}
}
El resultado de su ejecución es:
Valor (tipo valor) = 456
Valor (tipo object)= 123

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 = (string) o; // unboxing

Afortunadamente es posible conocer el tipo, de manera que si la conversión no es posible se


lanza una excepción. Pueden utilizarse los operadores is y as para determinar la corrección
de la conversión:

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.

Hashtable t = new Hashtable();

t.Add(0, "zero");
t.Add(1, "one");
t.Add(2, "two");

string s = string.Format("Your total was {0} on {1}",


total, date);

Desventajas del sistema unificado de tipos: Eficiencia.

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.

Recuerde la particularidad de este tipo sobre el operador de asignación: su funcionamiento es


como si fuera un tipo valor. Este es, realmente, el funcionamiento obtenido con el método
Copy(). Si no se desea obtener una copia (independiente) sino una copia en el sentido de un
tipo valor emplee el método Clone().

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.

string s = "!!Hola, mundo!!";;


for (int i=0; i < s.Length; i++)
Console.Write (s[i]);
Este ejemplo muestra todos los caracteres que componen la cadena, uno a uno. El ciclo realiza
tantas iteraciones como número de caracteres forman la cadena (su longitud) que se consulta
usando la propiedad Length.

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.

Los procedimientos de comparación y de búsqueda distinguen mayúsculas de minúsculas de


forma predeterminada. Pueden emplearse los métodos Compare() y Equals() para realizar
referencias combinadas y comparaciones entre valores de instancias de Object y String. Los
operadores relacionales == y != se implementan con el método Equals().

El método Concat() concatena una o más instancias de String o las representaciones de


tipo String de los valores de una o más instancias de Object. El operador + está
sobrecargado para realizar la concatenación. Por ejemplo, el siguiente código:

string s1 = "Esto es una cadena... como en C++";


string s2 = "Esto es una cadena... ";
string s3 = "como en C++";
string s4 = s2 + s3;
string s5 = String.Concat(s2, s3);

Console.WriteLine ("s1 = {0}", s1);


Console.WriteLine ("s4 = {0}", s4);
Console.WriteLine ("s5 = {0}", s5);

if ((s1 == s4) && (s1.Equals(s5)))


Console.WriteLine ("s1 == s4 == s5");
produce este resultado:
s1 = Esto es una cadena... como en C++
s4 = Esto es una cadena... como en C++
s5 = Esto es una cadena... como en C++
s1 == s4 == s5

Un método particularmente útil es Split(), que devuelve un vector de cadenas resultante de


"partir" la cadena sobre la que se aplica en palabras:

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:

 Realizar búsquedas en cadenas de caracteres: IndexOf(), LastIndexOf(),


 StartsWith(), EndsWith()
 Eliminar e insertar espacios en blanco: Trim(), PadLeft(),
 PadRight()
 Manipular subcadenas: Insert(), Remove(), Replace(), Substring(),
 Join()
 Modificar cadenas de caracteres: ToLower(), ToUpper(), Format() (al estilo del
printf de C, pero seguro).
Vectores y matrices
Un vector (matriz) en C# es radicalmente diferente a un vector (matriz) en C++: más que una
colección de variables que comparten un nombre y accesibles por índice, en C# se trata de
una instancia de la clase System.Array, y en consecuencia se trata de una colección que se
almacena en el heap y que está bajo el control del gestor de memoria.

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:

double [] array; // Declara un a referencia


// (no se instancia ningún
objeto)
array = new double[10]; // Instancia un objeto de la
clase
// System.Array y le asigna 10
casillas.

que combinadas resulta en (lo habitual):

double [] array = new double[10];

 El tamaño del vector se determina cuando se instancia, no es parte de la declaración.


 string [] texto; // OK
 string [10] texto; // Error
 La declaración emplea los paréntesis vacíos [ ] entre el tipo y el nombre para
determinar el número de dimensiones (rango).
 string [] Mat1D; // 1 dimension
 string [,] Mat2D; // 2 dimensiones
 string [,,] Mat3D; // 3 dimensiones
 string [,,,] Mat4D; // 4 dimensiones
 ......
 En C# el rango es parte del tipo (es obligatorio en la declaración). El número de
elementos no lo es (está asociado a la instancia concreta).
Otros ejemplos de declaración de vectores:

string[] a = new string[10]; // "a" es un vector de 10


cadenas
int[] primes = new int[9]; // "primes" es un vector
de 9 enteros
Un vector puede inicializarse a la misma vez que se declara. Las tres definiciones siguientes
son equivalentes:

int[] prime1 = new int[10] {1,2,3,5,7,11,13,17,19,23};


int[] prime2 = new int[] {1,2,3,5,7,11,13,17,19,23};
int[] prime3 = {1,2,3,5,7,11,13,17,19,23};

Los vectores pueden dimensionarse dinámicamente (en tiempo de ejecución). Por ejemplo, el
siguiente código:

Console.Write ("Num. casillas: ");


string strTam = Console.ReadLine();
int tam = Convert.ToInt32(strTam);

int[] VecDin = new int[tam];


for (int i=0; i<tam; i++) VecDin[i]=i;

Console.Write ("Contenido de VecDin = ");


foreach (int i in VecDin)
Console.Write(i + ", ");

Console.ReadLine();

produce este resultado:


Num. casillas: 6
Contenido de VecDin = 0, 1, 2, 3, 4, 5,

Esta facilidad es una gran ventaja, aunque una vez que el constructor actúa y se crea una
instancia de la clase System.Array

no es posible redimensionarlo. Si se desea una estructura de datos con esta funcionalidad


debe emplearse una estructura colección disponible en System.Collections (por elemplo,
System.Collections.ArrayList).

En el siguiente ejemplo observe cómo el vector palabras se declara como un vector de


string. Se instancia y se inicia después de la ejecución del método Split(), que devuelve
un vector de string.

string frase = "Esto es una prueba de particion";


string [] palabras; // vector de cadenas (no tiene
tamaño asignado)

palabras = frase.Split (null);

Console.WriteLine ("Frase = {0}", frase);


Console.WriteLine ("Hay = {0} palabras",
palabras.Length);

for (int i=0; i < palabras.Length; i++)


Console.WriteLine (" Palabra {0} = {1}", i,
palabras[i]);
El resultado es:

Frase = Esto es una prueba de particion


Hay = 6 palabras
Palabra 0 = Esto
Palabra 1 = es
Palabra 2 = una
Palabra 3 = prueba
Palabra 4 = de
Palabra 5 = particion

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.

int [,] array2D; // Declara un a referencia


// (no se instancia ningún
objeto)
array2D = new int [2,3]; // Instancia un objeto de la
clase
// System.Array y le asigna 6
casillas
// (2 filas con 3 columnas cada
una)

que combinadas (y con inicialización) podría quedar:

int [,] array2D = new int [2,3] {{1,0,4}, {3,2,5}};

El número de dimensiones puede ser, lógicamente, mayor que dos:

int [,,] array3D = new int [2,3,2];

El acceso se realiza con el operador habitual [ ], aunque el recorrido se simplica y clarifica en


C# con el ciclo foreach:

for (int i= 0; i< vector1.Length; i++)


vector1[i] = vector2[i];
...
foreach (float valor in vector2)
Console.Wtite (valor);

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.

Console.WriteLine ("Introduzca las dimensiones: ");


// Peticion de numero de filas (num. vectores)
Console.Write ("Num. Filas: ");
string strFils = Console.ReadLine();
int Fils = Convert.ToInt32(strFils);

// Declaracion de la tabla dentada: el numero de


// casillas de cada fila es deconocido.
int[][] TablaDentada = new int[Fils][];

// Peticion del numero de columnas de cada vector


for (int f=0; f<Fils; f++)
{
Console.Write ("Num. Cols. de fila {0}: ", f);
string strCols = Console.ReadLine();
int Cols = Convert.ToInt32(strCols);

// Peticion de memoria para cada fila


TablaDentada[f] = new int[Cols];
}

// Rellenar todas las casillas de la tabla dentada


for (int f=0; f<TablaDentada.Length; f++)
for (int c=0; c<TablaDentada[f].Length; c++)
TablaDentada[f][c]=((f+1)*10)+(c+1);

// 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,

 La herencia y aspectos relacionados (p.e. métodos virtuales, métodos abstractos) no


se admiten en los struct.
 No se puede especificar (explícitamente) un constructor sin parámetros. Los datos
struct tienen un constructor sin parámetros predefinido y no puede reemplazarse, sólo
se permiten constructores con parámetros. En este sentido son diferentes a los struct
en C++.
 En el caso de especificar algún constructor con parámetros deberíamos asegurarnos
de iniciar todos los campos.

En el siguiente ejemplo se proporciona un único constructor con parámetros:

public struct Point


{
public int x, y;

public Point(int x, int y)


{
this.x = x;
this.y = y;
}
public string Valor()
{
return ("[" + this.x + "," + this.y + "]");
}
}

Sobre esta declaración de tipo Point, observe estas declaraciones de datos Point:

Point p = new Point(2,5);


Point p2 = new Point();
Ambas crean una instancia de la clase Point en la pila y asigna los valores oportunos a los
campos del struct con el constructor:

 En el primer caso actúa el constructor suministrado en la implementación del tipo.


 En el segundo actúa el constructor por defecto, que inicia los campos numéricos a
cero, y los de tipo referencia a null.

Puede comprobarse fácilmente:


Console.WriteLine ("p = " + p.Valor());
Console.WriteLine ("p2 = " + p2.Valor());
produce como resultado:
p = [2,5]
p2 = [0,0]
En cambio, la siguiente declaración:

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.

Observe las siguientes operaciones con struct. Su comportamiento es previsible:

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

Console.WriteLine ("p = " + p.Valor());


Console.WriteLine ("p2 = " + p2.Valor());
Console.WriteLine ("p3 = " + p3.Valor());
el resultado es:

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.

La sintaxis es muy parecida a la empleada en C++:

enum State { Off, On };


aunque el punto y coma final es opcional ya que una enumeración es una definición de un
struct y ésta no requiere el punto y coma:
enum State { Off, On }

Al igual que en C++ y en C, C# numera a los elementos de la enumeración con valores


enteros sucesivos, asignando el valor 0 al primero de la enumeración, a menos que se
especifique explícitamente otra asignación:

enum Tamanio {
Pequeño = 1,
Mediano = 3,
Grande = 5
Inmenso
}
En el ejemplo, el valor asociado a Inmenso es 6.

Para acceder a los elementos de una enumeración debe cualificarse completamente:

Tamanio Ancho = Tamanio.Grande;


lo que refleja que cada enumeración es, en última instancia, un struct.

Si no se declara ningún tipo subyacente de forma explícita, se utiliza Int32. En el siguiente


ejemplo se especifica el tipo base byte:

enum Color: byte {


Red = 1,
Green = 2,
Blue = 4,
Black = 0,
White = Red | Green | Blue
}

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

Pueden convertirse explícitamente en su valor entero (como en C). Admiten determinados


operadores aritméticos (+,-,++,--) y lógicos a nivel de bits (&,|,^,~). ejemplo:

Color c2 = Color.White;
Console.WriteLine((int) c2); // 7
Console.WriteLine(c2.ToString()); // White

Otro ejemplo más complejo:

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.

Una clase es la definición de las características concretas de un determinado tipo de objetos:


cuáles son los datos y los métodos de los que van a disponer todos los objetos de ese tipo.
Se dice que los datos y los métodos son los miembros de la clase. En C# hay muchos tipos de
miembros, que podemos clasificar de manera muy genérica en las dos categorías antes
mencionadas: datos y 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.

Un método es una función, un conjunto de instrucciones al que se le asocia un nombre.

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:

Una clase muy sencilla (uso de this)

class Persona // Clase Persona


{
private string nombre; // campo privado

public Persona (string nombre) // Constructor


{
this.nombre = nombre; // acceso al campo privado
}

public void Presenta (Persona p) // Metodo


{
if (p != this) // Una persona no puede presentarse a si misma
Console.WriteLine("Hola, " + p.nombre + ", soy " +
this.nombre);
}
}
...
Persona yo = new Persona ("YO");
Persona tu = new Persona ("TU");
yo.Presenta(tu);
tu.Presenta(yo);
yo.Presenta(yo); // Sin efecto
tu.Presenta(tu); // Sin efecto

La ejecución del código anterior produce el siguiente resultado:

Hola, TU, soy YO


Hola, YO, soy TU

En el siguiente ejemplo mostramos más miembros de una clase:

Una clase sencilla (CocheSimple)

public class CocheSimple


{
private int VelocMax; // Campo
private string Marca; // Campo
private string Modelo; // Campo

// Método constructor sin argumentos


public CocheSimple () {
this.VelocMax = 0;
this.Marca = "Sin marca";
this.Modelo = "Sin modelo";
}

// Método constructor con argumentos


public CocheSimple (string marca, string mod, int velMax)
{
this.VelocMax = velMax;
this.Marca = marca;
this.Modelo = mod;
}

// 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.Write ("Mi coche: ");


MiCoche.MuestraCoche(); // LLamada al método "MuestraCoche()"

Console.Write ("El tuyo: ");


TuCoche.MuestraCoche(); // LLamada al método "MuestraCoche()"

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:

 Los tipos de nivel superior (aquéllos que se encuentran directamente en un


namespace) pueden ser public o internal
 Los miembros de una clase pueden ser public, private, protected, internal o
protected internal
 Los miembros de un struct pueden ser public, private o internal

Modificador de Un miembro del tipo T definido en el


acceso assembly A es accesible...
public desde cualquier sitio
private (por
sólo desde dentro de T (por defecto)
defecto)
protected desde T y los tipos derivados de T
internal desde los tipos incluidos en A
protected desde T, los tipos derivados de T y los tipos
internal incluidos en A
Variables de instancia y miembros estáticos
Por defecto, los miembros de una clase son variables de instancia: existe una copia de los
datos por cada instancia de la clase y los métodos se aplican sobre los datos de una instancia
concreta.

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 class MiClase


{
public const string version = "1.0.0";
public const string s1 = "abc" + "def";
public const int i3 = 1 + 2;
public const double PI_I3 = i3 * Math.PI;
public const double s = Math.Sin(Math.PI); //ERROR
...
}

Campos de sólo lectura (readonly)

Similares a las constantes, si bien su valor se inicializa en tiempo de ejecución (en su


declaración o en el constructor). A diferencia de las constantes, si cambiamos su valor no hay
que recompilar los clientes de la clase. Además, los campos de sólo lectura pueden ser
variables de instancia o variables estáticas.

public class MiClase


{
public static readonly double d1 = Math.Sin(Math.PI);
public readonly string s1;

public MiClase(string s)
{
s1 = s;
}
}
......
MiClase mio = new MiClase ("Prueba");
Console.WriteLine(mio.s1);
Console.WriteLine(MiClase.d1);
......

Produce como resultado:

Prueba
1,22460635382238E-16
Propiedades

Las propiedades son campos virtuales, al estilo de Delphi o C++Builder. Su aspecto es el de


un campo (desde el exterior de la clase no se diferencian) pero están implementadas con
código, como los métodos. Pueden ser de sólo lectura, de sólo escritura o de lectura y
escritura.

Considere de nuevo la clase CocheSimple. Podemos añadir la propiedad MaxSpeed:

// 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 ("Tunning my car...");


MiCoche.MaxSpeed = 200; // set
Console.WriteLine ("After tunning my car (Incr. max Speed to 200 Mph");
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)

My car's Max Speed: 137,5 Mph


Tunning my car...
After tunning my car (Incr. max Speed to 200 Mph
My car's Max Speed: 200 Mph

Mi coche: Citröen Xsara (320 Km/h)

Un ejemplo con campos, métodos y propiedades

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;
}

// Propiedad (solo lectura)


public double Velocidad
{
get { return this.velocidad; }
}

// 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
{

static void Main(string[] args)


{

Coche MiCoche = new Coche("Citröen", "Xsara Picasso",


"Rojo","1546876");

Console.WriteLine("Los datos de mi coche son:");


Console.WriteLine(" Marca: {0}", MiCoche.Marca);
Console.WriteLine(" Modelo: {0}", MiCoche.Modelo);
Console.WriteLine(" Color: {0}", MiCoche.Color);
Console.WriteLine(" Número de bastidor: {0}",
MiCoche.NumBastidor);
Console.WriteLine();

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.

void RefFunction (ref int p)


{
p++;
}
......

int x = 10;

RefFunction (ref x); // x vale ahora 11

El modificador out permite devolver valores a través de los argumentos de un método. De


esta forma, se permite que el método inicialice el valor de una variable. En cualquier caso, la
variable ha de tener un valor antes de terminar la ejecución del método. Igual que antes, Para
evitar problemas de mantenimiento, el modificador out hay que especificarlo tanto en la
definición del método como en el código que realiza la llamada.

void OutFunction(out int p)


{
p = 22;
}
......

int x; // x aún no está inicializada

OutFunction (out x);

Console.WriteLine(x); // x vale ahora 22

Sobrecarga de métodos: Como en otros lenguajes, el identificador de un método puede


estar sobrecargado siempre y cuando las signaturas de las distintas implementaciones del
método sean únicas (la signatura tiene en cuenta los argumentos, no el tipo de valor que
devuelven).

void Print(int i);


void Print(string s);
void Print(char c);
void Print(float f);

int Print(float f); // Error: Signatura duplicada


Vectores de parámetros: Como en C, un método puede tener un número variable de
argumentos. La palabra clave params permite especificar un parámetro de método que acepta
un número variable de argumentos. No se permiten parámetros adicionales después de la
palabra clave params, ni varias palabras clave params en una misma declaración de método.

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:

public static int Suma(params int[] intArr)


{
int sum = 0;
foreach (int i in intArr) sum += i;
return sum;
}
......
int sum1 = Sum(13,87,34); // sum1 vale 134
Console.WriteLine(sum1);
int sum2 = Sum(13,87,34,6); // sum2 vale 140
Console.WriteLine(sum2);
produce el siguiente resultado:
134
140

El siguiente código es algo más complejo.

public static void UseParams1(params int[] list)


{
for ( int i = 0 ; i < list.Length ; i++ )
Console.Write(list[i] + ", ");
Console.WriteLine();
}

public static void UseParams2(params object[] list)


{
for ( int i = 0 ; i < list.Length ; i++ )
Console.Write((object)list[i] + ", ");
Console.WriteLine();
}
......
UseParams1(1, 2, 3);
UseParams2(1, 'a', "test");

int[] myarray = new int[3] {10,11,12};


UseParams1(myarray);
Observe su ejecución:
1, 2, 3,
1, a, test,
10, 11, 12,
Constructores y "destructores"
Los constructores son métodos especiales que son invocados cuando se instancia una clase
(o un struct).

 Se emplean habitualmente para inicializar correctamente un objeto.


 Como cualquier otro método, pueden sobrecargarse.
 Si una clase no define ningún constructor se crea un constructor sin parámetros
(ímplícito).
 No se permite un constructor sin parámetros para los struct.

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 (+, -, *, /, %, &, |, ^, ==, !=, <, >, <=,
>=, <, >).

No se puede sobrecargar el acceso a miembros, la invocación de métodos, el operador de


asignación ni los operadores sizeof, new, is, as, typeof, checked, unchecked, &, || y
?:.

Los operadores && y || se evalúan directamente a partir de los operadores & y |.

La sobrecarga de un operador binario (v.g. *) sobrecarga implícitamente el operador de


asignación correspondiente (v.g. *=).

using System;

class OverLoadApp
{

public class Point


{
int x, y; // Campos

public Point() // Constructor sin parámetros


{
this.x = 0;
this.y = 0;
}
public Point(int x, int y) // Constructor común
{
this.x = x;
this.y = y;
}
public int X // Propiedad
{
get { return x; }
set { x = value; }
}
public int Y // Propiedad
{
get { return y; }
set { y = value; }
}

// Operadores de igualdad

public static bool operator == (Point p1, Point p2)


{
return ((p1.x == p2.x) && (p1.y == p2.y));
}
public static bool operator != (Point p1, Point p2)
{
return (!(p1==p2));
}

// 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);
}
}

static void Main(string[] args)


{
Point p1 = new Point(10,20);
Point p2 = new Point();
p2.X = p1.X;
p2.Y = p1.Y;

Point p3 = new Point(22,33);

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);

if (p1 == p2) Console.WriteLine ("p1 y p2 son


iguales");
else Console.WriteLine ("p1 y p2 son diferentes");

if (p1 == p3) Console.WriteLine ("p1 y p3 son


iguales");
else Console.WriteLine ("p1 y p3 son diferentes");

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

public static Point operator + (Point p1, Point p2) {


return SumaPoints (p1, p2);
}
public static Point operator - (Point p1, Point p2) {
return RestaPoints (p1, p2);
}
public static Point RestaPoints (Point p1, Point p2) {
return new Point(p1.x-p2.x, p1.y-p2.y);
}
public static Point SumaPoints (Point p1, Point p2) {
return new Point(p1.x+p2.x, p1.y+p2.y);
}

Y respecto a los operadores de comparación:

// Operadores de igualdad

public static bool operator == (Point p1, Point p2) {


return (p1.Equals(p2));
}
public static bool operator != (Point p1, Point p2) {
return (!p1.Equals(p2));
}
public override bool Equals (object o) { // Por valor
if ( (((Point) o).x == this.x) &&
(((Point) o).y == this.y) )
return true;
else return false;
}
public override int GetHashCode() {
return (this.ToString().GetHashCode());
}

El operador de asignación (=) cuando se aplica a clases copia la referencia, no el contenido. Si


se desea copiar instancias de clases lo habitual en C# es redefinir (overrride) el método
MemberwiseCopy() que heredan, por defecto, todas las clases en C# de System.Object.
Conversiones de tipo
Pueden programarse las conversiones de tipo, tanto explícitas como implícitas):

using System;

class ConversionesApp
{

public class Euro


{
private int cantidad; // Campo

public Euro (int v) // Constructor común


{ cantidad = v;}

public int valor // Propiedad


{ get { return cantidad; } }

// Conversión implícita "double <-- Euro"


public static implicit operator double (Euro x)
{
return ((double) x.cantidad);
}

// Conversión explícita "Euro <-- double"


public static explicit operator Euro(double x)
{
double arriba = Math.Ceiling(x);
double abajo = Math.Floor(x);
int valor = ((x+0.5 >= arriba) ? (int)arriba : (int)abajo);
return new Euro(valor);
}

} // class Euro

static void Main(string[] args)


{
double d1 = 442.578;
double d2 = 123.22;

Euro e1 = (Euro) d1; // Conversión explícita a "Euro"


Euro e2 = (Euro) d2; // Conversión explícita a "Euro"

Console.WriteLine ("d1 es {0} y e1 es {1}",


d1, e1.valor);
Console.WriteLine ("d2 es {0} y e2 es {1}",
d2, e2.valor);

double n1 = e1; // Conversión implícita "double <--Euro"


double n2 = e2; // Conversión implícita "double <--Euro"

Console.WriteLine ("n1 es {0}", n1);


Console.WriteLine ("n2 es {0}", n2);

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).

Observe las similitudes entre ambas en el siguiente ejemplo.

struct SPoint - class CPoint

using System;

struct SPoint
{
private int x, y; // Campos

public SPoint(int x, int y) // Constructor


{
this.x = x;
this.y = y;
}
public int X // Propiedad
{
get { return x; }
set { x = value; }
}
public int Y // Propiedad
{
get { return y; }
set { y = value; }
}
}

class CPoint
{
private int x, y; // Campos

public CPoint(int x, int y) // Constructor


{
this.x = x;
this.y = y;
}
public int X // Propiedad
{
get { return x; }
set { x = value; }
}
public int Y // Propiedad
{
get { return y; }
set { y = value; }
}
}

class Class2App
{
static void Main(string[] args)
{
SPoint sp = new SPoint(2,5);
sp.X += 100;
int spx = sp.X; // spx = 102

CPoint cp = new CPoint(2,5);


cp.X += 100;
int cpx = cp.X; // cpx = 102

Console.WriteLine ("spx es: {0} ", spx); // 102


Console.WriteLine ("cpx es: {0} ", cpx); // 102
Console.ReadLine ();
}
}

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.

Las clases que derivan de otras se definen usando la siguiente sintaxis:

class <claseHija> : <clasePadre>


{
<miembrosHija>
}

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.

C# sólo permite herencia simple.

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>)

Si no se incluye el compilador consideraría que vale :base(), lo que provocaría un error si la


clase base carece de constructor sin parámetros.
Ejemplo de "herencia" de constructores

public class B
{
private int h; // Campo

public B () { // Constructor sin parámetros


this.h = -1;
}
public B (int h) // Constructor con parámetro
{
this.h = h;
}
public int H // Propiedad
{
get { return h; }
set { h = value; }
}
} // class B

public class D : B // "D" hereda de "B"


{
private int i; // Campo

public D () : this(-1) {} // Constructor sin parámetros

public D (int i) { // Constructor con un parámetro


this.i = i;
}
public D (int h, int i) : base(h) { // Constructor con
this.i = i; // dos parámetros
}

public int I // Propiedad


{
get { return i; }
set { i = value; }
}

} // 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);

D varD1 = new D(); // Const. sin parámetros de D


D varD2 = new D(15); // Const. con 1 parámetro de D
D varD3 = new D(25, 11); // Const. con 2 parámetros de D

Console.WriteLine("varD1 : (I={0},H={1})", varD1.I,


varD1.H);
Console.WriteLine("varD2 : (I={0},H={1})", varD2.I,
varD2.H);
Console.WriteLine("varD3 : (I={0},H={1})", varD3.I,
varD3.H);
Console.ReadLine();
......
En el siguiente ejemplo se muestra cómo puede extenderse la clase CocheSimple vista
anteriormente para construir, a partir de ella, la clase Taxi. Observar como se emplea la
construcción base para referenciar a un constructor de la clase base y que cuando actúa el
constructor sin parámetros de la clase Taxi se llama implícitamente al constructor sin
parámetros de la clase CocheSimple.

Ejemplo: herencia sobre la clase CocheSimple

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;
}

public void MuestraCoche () {


Console.WriteLine (this.Marca + " " + this.Modelo +
" (" + this.VelocMax + " Km/h)");
}

} // class CocheSimple

class Taxi : CocheSimple


{
private string CodLicencia;

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 {

static void Main(string[] args) {

CocheSimple MiCoche =
new CocheSimple ("Citröen", "Xsara Picasso", 220);
CocheSimple TuCoche =
new CocheSimple ("Opel", "Corsa", 190);
CocheSimple UnCoche = new CocheSimple ();

Console.Write ("Mi coche: ");


MiCoche.MuestraCoche();
Console.Write ("El tuyo: ");
TuCoche.MuestraCoche();
Console.Write ("Un coche sin identificar: ");
UnCoche.MuestraCoche();

Console.WriteLine();

Taxi ElTaxiDesconocido = new Taxi ();


Console.Write ("Un taxi sin identificar: ");
ElTaxiDesconocido.MuestraCoche();

Taxi NuevoTaxi= new Taxi ("Citröen", "C5", 250, "GR1234");


Console.Write ("Un taxi nuevo: ");
NuevoTaxi.MuestraCoche();
Console.Write (" Licencia: {0}", NuevoTaxi.Licencia);

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);

Console.WriteLine ("Mi coche: " + MiCoche.ToString());

producen el siguiente resultado:

Mi coche: DemoHerencia.CocheSimple

lo que nos invita a redefinir el método ToString en la clase CocheSimple:

class CocheSimple
{
...
public override string ToString()
{
return (this.Marca + " " + this.Modelo +
" (" + this.VelocMax + " Km/h)");
}
...
}

Las dos instrucciones siguientes son equivalentes:

Console.WriteLine ("Mi coche: " + MiCoche.ToString());

Console.WriteLine ("Mi coche: " + MiCoche);

por lo que podemos sutituir las instrucciones que muestran los datos de los objetos
CocheSimple por:

Console.WriteLine ("Mi coche: " + MiCoche);


Console.WriteLine ("El tuyo: " + TuCoche);
Console.WriteLine ("Un coche sin identificar: " +
UnCoche);

y eliminamos el (innecesario) método MuestraCoche, el resultado de la ejecución del


programa anterior es:
La palabra reservada base sirve para hacer referencia a los miembros de la clase base que
quedan ocultos por otros miembros de la clase actual. Por ejemplo, podríamos redefinir
también el método ToString de la clase Taxi empleando el método redefinido ToString de
la clase base CocheSencillo:

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);

Taxi NuevoTaxi= new Taxi ("Citröen", "C5", 250, "GR1234");


Console.WriteLine ("Un taxi nuevo: " + NuevoTaxi);
......

y el resultado es:

En la sección dedicada a la sobrecarga de operadores introdujimos la clase Point. No había


ningún método que mostrara los datos de interés de un objeto de tipo Point. Podemos
sobreescribir el método ToString de manera que fuera:
public class Point
{
...
public override string ToString()
{
return ("["+this.X+", "+this.Y+"]");
}
...
}

Ahora las instrucciones de escritura se convierten en llamadas a este método, por ejemplo:

Console.WriteLine ("p1 es: " + p1);


// Console.WriteLine ("p1 es: " + p1.ToString()

El resultado de la ejecución de ese programa será:

Métodos virtuales

Un método es virtual si puede redefinirse en una clase derivada. Los métodos son no
virtuales por defecto.

 Los métodos no virtuales no son polimórficos (no pueden reemplazarse) ni


pueden ser abstractos.
 Los métodos virtuales se definen en una clase base (empleando la palabra
reservada virtual) y pueden ser reemplazados (empleando la palabra
reservada override) en las subclases (éstas proporcionan su propia -
específica- implementación).
 Generalmente, contendrán una implementación por defecto del método (si no,
se deberían utilizar métodos abstractos).

class Shape // Clase base


{
// "Draw" es un método virtual
public virtual void Draw() { ... }
}

class Box : Shape


{
// Reemplaza al método Draw de la clase base
public override void Draw() { ... }
}

class Sphere : Shape


{
// Reemplaza al método Draw de la clase base
public override void Draw() { ... }
}

void HandleShape(Shape s)
{
...
s.Draw(); // Polimorfismo
...
}

HandleShape(new Box());
HandleShape(new Sphere());
HandleShape(new Shape());

NOTA: Propiedades, indexadores y eventos también pueden ser virtuales.


Clases abstractas
Una clase abstracta es una clase que no puede ser instanciada. Se declara empelando la
palabra reservada abstract.

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.

abstract class Shape // Clase base abstracta


{
public abstract void Draw(); // Método abstracto
}

class Box : Shape


{
public override void Draw() { ... }
}

class Sphere : Shape


{
public override void Draw() { ... }
}

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.

Los struct en C# son implícitamente clases selladas.

¿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.

La sintaxis empleada para la definición de un indexador es similar a la de la definición de una


propiedad.

public class MiClase


{
...
public string this[int x]
{
get {
// Obtener un elemento
}
set {
// Fijar un elemento
}
}
...
}
...
MiClase MiObjeto = new MiClase();

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.

Algunas consideraciones finales:

 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;

public Coche (string marca, string modelo, int velocMax)


{
this.VelocMax = velocMax;
this.Marca = marca;
this.Modelo = modelo;
}

public override string ToString () {


return (this.Marca + " " + this.Modelo +
" (" + this.VelocMax + " Km/h)");
}

} // class Coche

public struct DataColeccionCoches


{
public int numCoches;
public int maxCoches;
public Coche[] vectorCoches;

public DataColeccionCoches (int max)


{
this.numCoches = 0;
this.maxCoches = max;
this.vectorCoches = new Coche[max];
}
} // struct DataColeccionCoches

public class Coches


{
// Los campos se encapsulan en un struct
DataColeccionCoches data;

// Constructor sin parámetros


public Coches()
{
this.data = new DataColeccionCoches(10);
// Si no se pone un valor se llamará al
// constructor sin parámetros (por defecto) del
// struct y tendremos problemas: no se puede
// explicitar el constructor sin parámetros
// para un struct.
}

// Constructor con parámetro


public Coches(int max)
{
this.data = new DataColeccionCoches(max);
}

public int MaxCoches


{
set { data.maxCoches = value; }
get { return (data.maxCoches); }
}
public int NumCoches
{
set { data.numCoches = value; }
get { return (data.numCoches); }
}
public override string ToString () {
string str1 = " --> Maximo= " + this.MaxCoches;
string str2 = " --> Real = " + this.NumCoches;
return (str1 + "\n" + str2);
}

// El indexador devuelve un objeto Coche de acuerdo


// a un índice numérico
public Coche this[int pos]
{
// Devuelve un objeto del vector de coches
get
{
if(pos < 0 || pos >= MaxCoches)
throw new
IndexOutOfRangeException("Fuera de rango");
else
return (data.vectorCoches[pos]);
}
// Escribe en el vector
set { this.data.vectorCoches[pos] = value;}
}

} // class Coches

class IndexadorCochesApp
{
static void Main(string[] args)
{
// Crear una colección de coches
Coches MisCoches = new Coches (); // Por defecto (10)

Console.WriteLine ("***** Mis Coches *****");


Console.WriteLine ("Inicialmente: ");
Console.WriteLine (MisCoches);

// Añadir coches. Observar el acceso con [] ("set")


MisCoches[0] = new Coche ("Opel", "Zafira", 200);
MisCoches[1] = new Coche ("Citröen", "Xsara", 220);
MisCoches[2] = new Coche ("Ford", "Focus", 190);

MisCoches.NumCoches = 3;

Console.WriteLine ("Despues de insertar 3 coches: ");


Console.WriteLine (MisCoches);

// 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 ();

// *********************************************

// Crear una colección de coches


Coches TusCoches = new Coches (4);

Console.WriteLine ("***** Tus Coches *****");


Console.WriteLine ("Inicialmente: ");
Console.WriteLine (TusCoches);

// Añadir coches. Observar el acceso con []


TusCoches[TusCoches.NumCoches++] =
new Coche ("Opel", "Corsa", 130);
TusCoches[TusCoches.NumCoches++] =
new Coche ("Citröen", "C3", 140);

Console.WriteLine ("Despues de insertar 2 coches: ");


Console.WriteLine (TusCoches);

// 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.

La interfaz no contiene implementación alguna. La clase o struct que implementa el interfaz es


la que tiene la funcionalidad especificada por 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.

 La especificación del interfaz puede incluir métodos, propiedades, indexadores y


eventos, pero no campos, operadores, constructores o destructores.
 Aunque solo se permite la herencia simple de clases, como ocurre en Java, se
permite y herencia múltiple de interfaces. Esto significa que es posible definir tipos
que deriven de más de una interfaz.
 Los interfaces (como algo separado de la implementación) permiten la existencia del
polimorfismo, al poder existir muchas clases o structs que implementen el interfaz.

Ejemplo de polimorfismo

using System;

namespace Interface1
{
class Interface1App
{
// Definición de una interface

public interface IDemo


{
void MetodoDeIDemo ();
}

// "Clase1" y "Clase2" implementan la interface

public class Clase1 : IDemo


{
public void MetodoDeIDemo()
{
Console.WriteLine ("Método de Clase1");
}
}

public class Clase2 : IDemo


{
public void MetodoDeIDemo()
{
Console.WriteLine ("Método de Clase2");
}
}

static void Main(string[] args)


{
Clase1 c1 = new Clase1();
Clase2 c2 = new Clase2();
IDemo demo; // objeto de una interface

// Ejemplo de polimorfismo
demo = c1;
demo.MetodoDeIDemo();

demo = c2;
demo.MetodoDeIDemo();

Console.ReadLine();
}
}
}

Otro ejemplo:

Ejemplo de interface "heredada"

using System;

class InterfaceApp
{

interface IPresentable
{
void Presentar();
}

class Triangulo : IPresentable


{
private double b, a;
public Triangulo(double Base, double altura)
{
this.b=Base;
this.a=altura;
}
public double Base
{
get { return b; }
}
public double Altura
{
get { return a; }
}
public double Area
{
get { return (Base*Altura/2); }
}
public void Presentar()
{
Console.WriteLine("Base del triángulo: {0}", Base);
Console.WriteLine("Altura del triángulo: {0}", Altura);
Console.WriteLine("Área del triángulo: {0}", Area);
}
}
class Persona : IPresentable
{
private string nbre, apell, dir;
public Persona (string nombre, string apellidos,
string direccion)
{
this.nbre = nombre;
this.apell = apellidos;
this.dir = direccion;
}
public string Nombre
{
get { return nbre; }
}
public string Apellidos
{
get { return apell; }
}
public string Direccion
{
get { return dir; }
}
public void Presentar()
{
Console.WriteLine("Nombre: {0}", Nombre);
Console.WriteLine("Apellidos: {0}", Apellidos);
Console.WriteLine("Dirección: {0}", Direccion);
}
}

static void VerDatos(IPresentable IP)


{
IP.Presentar();
}

static void Main(string[] args)


{
Triangulo t=new Triangulo(10,5);
Persona p=new Persona ("Paco", "Pérez", "su casa");

Console.WriteLine("Ya se han creado los objetos");


Console.WriteLine("\nINTRO para VerDatos(triangulo)");
Console.ReadLine();
VerDatos(t);

Console.WriteLine("\nINTRO para VerDatos(proveedor)");


Console.ReadLine();
VerDatos(p);

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.

La sobrecarga de los operadores relacionales puede hacerse implementando la interface


IComparable, concretamente el método CompareTo. La interface IComparable define un
método de comparación generalizado, implementado por un tipo de valor o clase para crear un
método de comparación específico del tipo. Esta interfaz la implementan tipos cuyos valores se
pueden ordenar, como por ejemplo, las clases numéricas o de tipo cadena (las clases enum y
string implementan esta interface). Un tipo de valor o clase implementa el método
CompareTo para crear un método de comparación específico.

El siguiente ejemplo amplía la especificación de la clase Point introducida anteriormente.

public class Point : IComparable {

......

// Operadores relacionales

public int CompareTo(object o) {


Point tmp = (Point) o;
if (this.x > tmp.x) return 1;
else
if (this.x < tmp.x) return -1;
else return 0;
}

public static bool operator < (Point p1, Point p2) {


IComparable ic1 = (IComparable) p1;
return (ic1.CompareTo(p2) < 0);
}
public static bool operator > (Point p1, Point p2) {
IComparable ic1 = (IComparable) p1;
return (ic1.CompareTo(p2) > 0);
}
public static bool operator <= (Point p1, Point p2) {
IComparable ic1 = (IComparable) p1;
return (ic1.CompareTo(p2) <= 0);
}
public static bool operator >= (Point p1, Point p2) {
IComparable ic1 = (IComparable) p1;
return (ic1.CompareTo(p2) >= 0);
}

......
} // class Point

static void Main(string[] args)


{
......
if (p1 > p2) Console.WriteLine ("p1 > p2");
if (p1 >= p2) Console.WriteLine ("p1 >= p2");
if (p1 < p2) Console.WriteLine ("p1 < p2");
if (p1 <= p2) Console.WriteLine ("p1 <= p2");
if (p1 == p2) Console.WriteLine ("p1 == p2");
Console.WriteLine ();

if (p1 > p3) Console.WriteLine ("p1 > p3");


if (p1 >= p3) Console.WriteLine ("p1 >= p3");
if (p1 < p3) Console.WriteLine ("p1 < p3");
if (p1 <= p3) Console.WriteLine ("p1 <= p3");
if (p1 == p3) Console.WriteLine ("p1 == p3");
Console.WriteLine ();
}
}

Herencia múltiple

La plataforma .NET no permite herencia múltiple de implementación, aunque sí se puede


conseguir herencia múltiple de interfaces. Clases, structs e interfaces pueden heredar de
múltiples interfaces (como en Java).

Herencia de interfaces

using System;

namespace Interface2
{
class Interface2App
{
// Definición de interfaces

public interface IDemo1


{
void Metodo1DeInterface1 ();
string Metodo2DeInterface1 ();
}

public interface IDemo2


{
void Metodo1DeInterface2 ();
}

public interface IDemo3 : IDemo1


{
void Metodo1DeInterface3 (string mensaje);
}
// "Clase1" implementan la interface "IDemo1"

public class Clase1 : IDemo1


{
public void Metodo1DeInterface1()
{ Console.WriteLine ("Mét1 de Int1 en Clase1"); }

public string Metodo2DeInterface1()


{ return ("En Mét2 de Int1 en Clase1"); }

// "Clase1" implementan las interfaces


// "IDemo1" e "IDemo2"

public class Clase2 : IDemo1, IDemo2


{
public void Metodo1DeInterface1()
{ Console.WriteLine ("Mét1 de Int1 en Clase2"); }

public string Metodo2DeInterface1()


{ return ("En Mét2 de Int1 en Clase2"); }

public void Metodo1DeInterface2()


{ Console.WriteLine ("Mét1 de Int2 en Clase2"); }
}

// "Clase3" implementan la interface "IDemo3", la


// cual ha heredado de "IDemo1"

public class Clase3 : IDemo3


{
public void Metodo1DeInterface1()
{ Console.WriteLine ("Mét1 de Int1 en Clase3"); }

public string Metodo2DeInterface1()


{ return ("En Mét2 de Int1 en Clase3"); }

public void Metodo1DeInterface3 (string m)


{ Console.WriteLine (m + "Mét1 de Int3 en Clase3"); }

}
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();
}
}
}

Resolución de conflictos de nombres


Si dos interfaces tienen un método con el mismo nombre, se especifica explícitamente el
interfaz al que corresponde la llamada al método para eliminar ambigüedades

interface IControl
{
void Delete();
}

interface IListBox: IControl


{
void Delete();
}

interface IComboBox: ITextBox, IListBox


{
void IControl.Delete();
void IListBox.Delete();
}
Delegados
Un delegado es un tipo especial de clase que define la signatura y el tipo de retorno de un
método.

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.

delegate double Del (double x);

static double incremento (double x)


{ return (++x); }

static void Main(string[] args)


{
// Instanciación
Del del1 = new Del (Math.Sin);
Del del2 = new Del (Math.Cos);
Del del3 = new Del (incremento);

// 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:

static void Main(string[] args)


{
Del f1 = new Del (Math.Sin);
Del f2 = new Del (Math.Cos);
Del f3 = new Del (incremento);

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.

Finalmente, observe cómo el polimorfismo es más evidente en el siguiente código:

static void Main(string[] args)


{
Del[] d = new Del[3];

d[0] = new Del (Math.Sin);


d[1] = new Del (Math.Cos);
d[2] = new Del (incremento);

for (int i=0; i<3; i++)


Console.WriteLine (d[i](0)); // 0, 1, 1
}

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
{

public delegate void Print (string s);

public static void EnConsola (string str)


{
Console.WriteLine(str);
}

public static void EnFichero (string str)


{
StreamWriter fo = new StreamWriter ("Resultado.txt");
fo.WriteLine(str);
fo.Close();
}

public static void Display (Print Metodo)


{
Metodo ("Este es el mensaje que se muestra");
}

static void Main()


{
Print dc = new Print (EnConsola);
Print df = new Print (EnFichero);
Display (dc);
Display (df);

Console.ReadLine();
}
}

En el ejemplo anterior EnFichero y EnConsola son métodos que reciben un argumento


string y no devuelven nada. El tipo delegado Print representa a métodos que reciben un
argumento string y no devuelven nada, por lo que los métodos EnFichero y EnConsola
pueden delegar en objetos de tipo Print. En este ejemplo, no obstante, el método Display
recibe un argumento del tipo delegado Print, esto es, recibe la referencia a un método que
recibe un argumento string y no devuelve nada (EnFichero y EnConsola son buenos
candidatos).

Las instrucciones:

Print dc = new Print (EnConsola);


Print df = new Print (EnFichero);
hacen que los métodos EnConsola y EnFichero deleguen en los objetos dc y df,
respectivamente. Así, podemos usarlos como argumentos al método Display sin problemas:
Display (dc);
Display (df);

Delegados multimiembro (Multicasting)

Un delegado multimiembro es un delegado individual compuesto de más delegados. En


definitiva: permite almacenar referencias a uno o más métodos, de tal manera que a través
del objeto delegado sea posible solicitar la ejecución en cadena de todos ellos.

Los delegados multimiembro tienen dos restricciones:

 No pueden devolver ningún valor (deben ser void).


 No pueden tener parámetros de salida (out).

Se añaden delegados a un delegado multimiembro con el operador + y se eliminan delegados


de un delegado multimiembro con el operador -. Cada delegado tiene una lista ordenada de
métodos que se invocan secuencialmente (en el mismo orden en el que fueron añadidos al
delegado).

"Multicasting"

using System;

class Delegate2App
{
delegate void SomeEvent (void);

static void Func1(void)


{ Console.WriteLine(" Desde Func1"); }

static void Func2(void)


{ Console.WriteLine(" Desde Func2"); }

static void Main(string[] args)


{
SomeEvent func = new SomeEvent(Func1);

func += new SomeEvent(Func2);

Console.WriteLine("Llamada a func");
func(); // Se llama tanto a Func1 como a Func2

func -= new SomeEvent(Func1);

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 class EjemploDelegado


{
public Calculo Calculo1;
public Calculo Calculo2;

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);
}

static void Main(string[] args)


{
int resultado = 0;
EjemploDelegado Del = new EjemploDelegado ();

Del.Calculo1 = new Calculo (Del.Suma);


Del.Calculo2 = new Calculo (Del.Resta);

Del.Calculo1 (5, 9, ref resultado);


Del.Calculo2 (5, 9, ref resultado);
Console.WriteLine();

Calculo MultiCalc = Del.Calculo1;


MultiCalc += Del.Calculo2;
MultiCalc (5, 9, ref resultado);
Console.WriteLine();

MultiCalc -= Del.Calculo1;
MultiCalc (5, 9, ref resultado);

Console.ReadLine();
}
}

Delegados vs. interfaces

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.

El uso de eventos, no obstante, no está limitado a la implementación de interfaces. También


son útiles en el desarrollo de aplicaciones que deban realizar operaciones periódicamente o
realizar operaciones de forma asíncrona (p.ej. llegada de un correo electrónico, terminación de
una operación larga...).

El lenguaje C# da soporte a los eventos mediante el uso de delegados. Al escribir nuestra


aplicación, un evento no será más que un campo que almacena un delegado. Los usuarios de
la clase podrán registrar delegados (mediante los operadores += y -=), pero no podrán
invocar directamente al delegado.

Eventos en C#

public delegate void EventHandler ( object sender, EventArgs e);

public class Button


{
public event EventHandler Click;

protected void OnClick (EventArgs e)


{
// This is called when button is clicked
if (Click != null) Click(this, e);
}
}

public class MyForm: Form


{
Button okButton;

static void OkClicked(object sender, EventArgs e)


{
ShowMessage("You pressed the OK button");
}

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.

Una excepción es cualquier situación de error o comportamiento inesperado que encuentra un


programa en ejecución. Las excepciones se pueden producir a causa de un error en el código o
en código al que se llama (como una biblioteca compartida), que no estén disponibles recursos
del sistema operativo, condiciones inesperadas que encuentra Common Language Runtime
(por ejemplo, código que no se puede comprobar), etc. La aplicación se puede recuperar de
algunas de estas condiciones, pero de otras no.

Las excepciones pueden generarse en un proceso o hebra de nuestra aplicación (con la


sentencia throw) o pueden provenir del entorno de ejecución de la plataforma .NET.

En .NET Framework, una excepción es un objeto derivado de la clase Exception. El


mecanismo de control de excepciones en C# es muy parecido al de C++ y Java: la excepción
se inicia en un área del código en que se produce un problema. La excepción asciende por la
pila hasta que la aplicación la controla o el programa se detiene.

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

Se puede profundizar en el tratamiento de la excepción, por ejemplo, comprobando alguna


propiedad del objeto Exception generado.

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");
}

La ejecución de este programa produce el siguiente resultado:

try
catch
Excepción detectada: Mi excepcion
finally

La mayoría de las clases derivadas de la clase Exception no implementan miembros


adicionales ni proporcionan más funcionalidad, simplemente heredan de Exception. Por ello,
la información más importante sobre una excepción se encuentra en la jerarquía de
excepciones, el nombre de la excepción y la información que contiene la excepción.

Ejemplo 3

El siguiente ejemplo muestra cómo el uso de execpciones puede controlar un número


importante de situaciones de error.

static void Main(string[] args)


{
int numerador = 10;
Console.WriteLine ("Numerador es = {0}", numerador);
Console.Write ("Denominador = ");
string strDen = Console.ReadLine();

int denominador, cociente;

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();
}
}

Cuando todo funciona sin problemas:

Cuando se intenta dividir por cero:


Cuando se produce desbordamiento:

Cuando se produce otro problema (cadena vacía, por ejemplo):

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);

Console.Write ("Denominador = ");


string strDen = Console.ReadLine();

int denominador, cociente;

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();

StackTrace st = new StackTrace(e, true);

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()

public class Test


{
private int n;

public Test (int n)


{
this.n = n;
}
}

// Acceso a información acerca de una clase

public static void Main(string[] args)


{
Type tipoClase = typeof(Test);
Console.WriteLine("El nombre del tipo de tipoClase es: {0}",
tipoClase.Name);

Test t = new Test(0);


Type tipoVariable = t.GetType();
Console.WriteLine("El tipo de la variable t es: {0}",
tipoVariable.Name);
}

El programa anterior muestra como resultado:

El nombre del tipo de tipoClase es: Test


El tipo de la variable t es: Test

Otro ejemplo del uso de Type y GetType():

Un ejemplo más complejo 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:

Métodos de una clase

using System;
using System.Reflection;

class ReflectApp
{
public class Test
{
private int n;

public Test (int n) {


this.n = n;
}
public void Metodo1DeTest (int n) {
// .....
}
public int Metodo2DeTest (int a, float b, string c) {
// .....
return 0;
}
}
public static void Main(string[] args)
{
Type t = typeof(Test);

MethodInfo[] MetInf = t.GetMethods();

foreach (MethodInfo m in MetInf) {


Console.WriteLine ();
Console.WriteLine ("Método: " + m.Name );
Console.WriteLine (" Características: "
+ ((m.IsPublic) ? " (public)" : "")
+ ((m.IsVirtual) ? " (virtual)" : ""));

// Parámetros

ParameterInfo[] ParInf = m.GetParameters();

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

Declarar un atributo en C# es simple: se utiliza la forma de una declaración de clase que


hereda de System.Attribute y que se ha marcado con el atributo AttributeUsage, como
se indica a continuación:

// La clase HelpAttribute posee un parámetro posicional (url)


// de tipo string y un parámetro con nombre -opcional- (Topic)
// de tipo string.

[AttributeUsage(AttributeTargets.All)]
public class HelpAttribute: Attribute
{
public string Topic = null;
private string url;

public HelpAttribute(string url)


{
this.url = url;
}
public string Url {
get { return url; }
}
public override string ToString() {
string s1 = " Url = " + this.Url;
string dev = (this.Topic != null) ?
(s1 + " - Topic: " + this.Topic) : s1;
return (dev);
}
} // class HelpAttribute

 El atributo AttributeUsage especifica los elementos del lenguaje a los que


se puede aplicar el atributo.
 Las clases de atributos son clases públicas derivadas de System.Attribute
que disponen al menos de un constructor público.
 Las clases de atributos tienen dos tipos de parámetros:
o Parámetros posicionales, que se deben especificar cada vez que se
utiliza el atributo. Los parámetros posicionales se especifican como
argumentos de constructor para la clase de atributo. En el ejemplo
anterior, url es un parámetro posicional.
o Parámetros con nombre, los cuales son opcionales. Si se especifican
al usar el atributo, debe utilizarse el nombre del parámetro. Los
parámetros con nombre se definen mediante un campo o una
propiedad no estáticos. En el ejemplo anterior, Topic es un parámetro
con nombre.
 Los parámetros de un atributo sólo pueden ser valores constantes de los
siguientes tipos:
o Tipos simples (bool, byte, char, short, int, long, float y
double)
o string
o System.Type
o enumeraciones
o object (El argumento para un parámetro de atributo del tipo object
debe ser un valor constante de uno de los tipos anteriores.)
o Matrices unidimensionales de cualquiera de los tipos anteriores

Parámetros para el atributo AttributeUsage

El atributo AttributeUsage proporciona el mecanismo subyacente mediante el cual los


atributos se declaran.

AttributeUsage tiene un parámetro posicional:

 AllowOn, que especifica los elementos de programa a los que se puede


asignar el atributo (clase, método, propiedad, parámetro, etc.). Los valores
aceptados para este parámetro se pueden encontrar en la enumeración
System.Attributes.AttributeTargets de .NET Framework. El valor
predeterminado para este parámetro es el de todos los elementos
del programa (AttributeElements.All).

AttributeUsage tiene un parámetro con nombre:

 AllowMultiple, valor booleano que indica si se pueden especificar varios


atributos para un elemento de programa. El valor predeterminado para este
parámetro es False.

Utilizar una clase atributo

A continuación, se muestra un breve ejemplo de uso del atributo declarado en la sección


anterior:

[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).

Acceder a los atributos por reflexión

Los atributos de un tipo o de un miembro de un tipo pueden ser examinados en tiempo de


ejecución (reflexión), heredan de la clase System.Attribute y sus argumentos se
comprueban en tiempo de compilación.

Los principales métodos de reflexión para consultar atributos se encuentran en la clase


System.Reflection.MemberInfo. El método clave es GetCustomAttributes, que
devuelve un vector de objetos que son equivalentes, en tiempo de ejecución, alos atributos del
código fuente.

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

Este ejemplo amplía el anterior añadiendo muchas más posibilidades.

Atributos y reflexión

using System;
using System.Diagnostics;
using System.Reflection;

// La clase IsTested es una clase de atributo


// definida por el usuario.
// Puede aplicarse a cualquier definición, incluyendo:
// - tipos (struct, class, enum, delegate)
// - miembros (métodos, campos, events, properties, indexers)
// Se usa sin argumentos.

[AttributeUsage(AttributeTargets.All)]
public class IsTestedAttribute : Attribute {
public override string ToString() { return (" REVISADO"); }
}

// La clase HelpAttribute posee un parámetro posicional (url)


// de tipo string y un parámetro con nombre -opcional- (Topic)
// de tipo string.

[AttributeUsage(AttributeTargets.All)]
public class HelpAttribute: Attribute {
public string Topic = null;
private string url;

public HelpAttribute(string url) {


this.url = url;
}
public string Url {
get { return url; }
}
public override string ToString()
{
string s1 = " Url = " + this.Url;
string dev = (this.Topic != null) ?
(s1 + ". Topic = " + this.Topic) : s1;
return (dev);
}

// La clase CodeReviewAttribute es una clase de atributo


// definida por el usuario.
// Puede aplicarse en clases y structs únicamente.
// Toma dos argumentos de tipo string (el nombre del
// revisor y la fecha de revisión) además de permitir otro
// argumento opcional (Comment) de tipo string.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class CodeReviewAttribute: System.Attribute
{
public CodeReviewAttribute(string reviewer, string date) {
this.reviewer = reviewer;
this.date = date;
this.comment = "";
}

public string Comment {


get { return(comment); }
set { comment = value; }
}

public string Date {


get { return(date); }
}

public string Reviewer {


get { return(reviewer); }
}

public override string ToString()


{
string st1 = " Revisor : " + Reviewer + "\n";
string st2 = " Fecha: " + Date;
string st3;
if (Comment.Length != 0)
st3 = "\n" + " NOTAS: " + Comment;
else st3 = "";
return (st1 + st2 + st3);
}
string reviewer;
string date;
string comment;
}

[CodeReview("Pepe", "01-12-2002", Comment="Codigo mejorable")]


[Help("http://decsai.ugr.es/Clase1.htm")]
[IsTested]
class Clase1
{
int c1c1;
int c2c1;

public Clase1 (int n1, int n2)


{
this.c1c1 = n1;
this.c2c1 = n2;
}

[IsTested]
public override string ToString()
{
return (this.c1c1.ToString() + this.c2c1.ToString());
}
}

[CodeReview("Juani", "12-11-2002", Comment="Excelente")]


[Help("http://decsai.ugr.es/Clase3.htm", Topic="Atributos")]
class Clase2
{
string c1c2;

public Clase2 (string s) {


this.c1c2 = s;
}

[IsTested]
public char Met1Clase2 ()
{
return (this.c1c2[0]);
}
}

[CodeReview("Pepe", "12-11-2002"), IsTested()]


class Clase3
{
int c1c3;

[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;
}

private static string InfoRevision (MemberInfo member)


{
if (IsMemberTested(member)) return ("REVISADO");
else return ("NO REVISADO");
}

private static void DumpAttributes(MemberInfo member)


{
Console.WriteLine();
Console.WriteLine("Información de: " + member.Name);
/*
object[] arr =
member.GetCustomAttributes(typeof(HelpAttribute), true);
if (arr.Length == 0)
Console.WriteLine("Esta clase no tiene ayuda.");
else {
HelpAttribute ha = (HelpAttribute) arr[0];
Console.WriteLine (ha.ToString());
}
*/
foreach (object attribute in member.GetCustomAttributes(true))
{
if (attribute is HelpAttribute)
Console.WriteLine (" Atributos de ayuda:");
if (attribute is CodeReviewAttribute)
Console.WriteLine (" Atributos de Revisión:");
if (attribute is IsTestedAttribute)
Console.WriteLine (" Atributos de Actualización:");

Console.WriteLine(attribute);
}
}

static void Main(string[] args)


{
// t es un vector de tipos
Type [] t = new Type[3];

t[0] = typeof(Clase1);
t[1] = typeof(Clase2);
t[2] = typeof(Clase3);

for (int i=0; i<3; i++) {

DumpAttributes(t[i]);

Console.WriteLine (" Información de los métodos:");

foreach (MethodInfo m in (t[i]).GetMethods()) {


if (IsMemberTested(m)) {
Console.WriteLine(" Método {0} REVISADO",
m.Name);
}
else {
Console.WriteLine(" Método {0} NO REVISADO",
m.Name);
}
}

}
Console.ReadLine();
}
}
Programas en C#

Organización lógica de los tipos


Organización física de los tipos
Ejecución de aplicaciones
Código no seguro
Preprocesador
Documentación

El compilador de C# se ocupa de abstraer al programador de la localización (ficheros) de los


tipos y otros elementos. Es irrelevante el hecho de colocar el código en un único fichero o de
disponerlo en varios, así como de usar una clase antes de declararla: el compilador se
encargará de encontrar la localización de los elementos. La consecuencia es que no existe,
propiamente, el concepto de enlace (linking): el compilador compila el código a un ensamblado
(o simplemente a un módulo).

Organización lógica de los tipos


Del mismo modo que los ficheros se organizan en directorios, los tipos de datos se organizan
en espacios de nombres (del inglés, namespaces).

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.

En definitiva: Los espacios de nombres proporcionan una forma unívoca de identificar


un tipo. Eliminan cualquier tipo de ambigüedad en los nombres de los símbolos empleados en
un programa.

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;

static void Main(string[] args)


{
...
}
}

No existe relación entre espacios de nombres y ficheros (a diferencia de Java).

Los espacios de nombres se pueden anidar. Observe en el siguiente ejemplo cómo la


cualificación evita cualquier duda acerca de la clase de los objetos:

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;

static void Main(string[] args)


{
...
}
}

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

//C2 c; // ¡Error! C2 no está definida en N1


C1.C2 o2; // Idem a: N1.C1.C2 o2;
N1.N2.C2 o3; // Tipo totalmente cualificado
N2.C2 o3_bis; // Idem a: N1.N2.C2 o3_bis;

static void Main(string[] args)


{
...
}
}
}

La directiva using también puede usarse para crear un alias para un espacio de nombres
(alias using).

using Cl2 = N1.N2.C2;


using NS1 = N1.N2;

Cl2 ob1; // O sea, N1.N2.C2


NS1.C2 ob2; // O sea, N1.N2.C2

Observe cómo, en ocasiones, el uso de alias simplifica y clarifica el código:

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;

static void Main(string[] args)


{

}
}
}

Cualquier declaración -propia- que use un nombre empelado en un espacio de nombres


oscurece la declaración del espacio de nombres en el bloque en el que ha sido declarada.

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

Los tipos se definen en ficheros

 Un fichero puede contener múltiples tipos.


 Cada tipo está definido en un único fichero (declaración y definición
coinciden).
 No existen dependencias de orden entre los tipos.

Los ficheros se compilan en módulos.

En .NET existen dos tipos de módulos de código compilado:

 Ejecutables (extensión .exe)


 Bibliotecas de enlace dinámico (extensión .dll)

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 módulos se agrupan en ensamblados o assemblies:

Un ensamblado consiste en un bloque constructivo reutilizable, versionable y autodescriptivo


de una aplicación de tipo Common Language Runtime. Los ensamblados proporcionan la
infraestructura que permite al motor de tiempo de ejecución comprender completamente el
contenido de una aplicación y hacer cumplir las reglas del control de versiones y de
dependencia definidas por la aplicación. Estos conceptos son cruciales para resolver el
problema del control de versiones y para simplificar la implementación de aplicaciones en
tiempo de ejecución.

Un ensamblado es una agrupación lógica de uno o más módulos o ficheros de recursos


(ficheros .GIF, .HTML, etc.) que se engloban bajo un nombre común. Un programa puede
acceder a información o código almacenados en un ensamblado sin tener que conocer cuál es
el fichero en concreto donde se encuentran, por lo que los ensamblados nos permiten
abstraernos de la ubicación física del código que ejecutemos o de los recursos que usemos.

Hay dos tipos de ensamblados: ensamblados privados y ensamblados compartidos. También


para evitar problemas, se pueden mantener múltiples versiones de un mismo ensamblado. Así,
si una aplicación fue compilada usando una cierta versión de un determinado ensamblado
compartido, cuando se ejecute sólo podrá hacer uso de esa versión del ensamblado y no de
alguna otra más moderna que se hubiese instalado. De esta forma se soluciona el problema
del infierno de las DLL.
Referencias

En Visual Studio se utilizan referencias para identificar assemblies particulares, p.ej.


compilador de C#

csc HelloWorld.cs /reference:System.WinForms.dll

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().

 1. Añadir al proyecto un módulo de código (clase). Escribir una clase "útil" e


insertarla en un espacio de nombres. Supongamos se llama MiClase.cs
 2. Compilar el espacio de nombres y obtener una DLL:

csc /t:library MiClase.cs

 3. Escribir en Main() código que use la clase. Supongamos que el fichero se


llama Ppal.cs
 4. Compilar el módulo principal con el espacio de nombres y crear un
ejecutable:

csc /r:MiClase.dll Ppal

Observar el uso de una referencia en la llamada al compilador.


Ejecución de aplicaciones
Gestión de memoria

C# utiliza un recolector de basura para gestionar automáticamente la memoria, lo que elimina


quebraderos de cabeza y una de las fuentes más comunes de error pero conlleva una
finalización no determinísitica (no se ofrece ninguna garantía respecto a cuándo se llama a un
destructor, ni siquiera podemos afirmar que llegue a llamarse el destructor).

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:

public class MyResource : IDisposable


{
public void MyResource()
{
// Acquire valuble resource
}

public void Dispose()


{
// Release valuble resource
}

public void DoSomething()


{
...
}

using (MyResource r = new MyResource())


{
r.DoSomething();
} // se llama a r.Dispose()
Código no seguro
En ocasiones necesitamos tener un control total sobre la ejecución de nuestro código
(cuestiones de rendimiento, compatibilidad con código existente, uso de DLLs...), por lo que
C# nos da la posibilidad de marcar fragmentos de código como código no seguro (unsafe) y
así poder emplear C/C++ de forma nativa: punteros, aritmética de punteros, operadores -> y
*, ... sin recolección de basura. La instrucción stackalloc reserva memoria en la pila de
manera similar a como malloc lo hace en C o new en C++.

public unsafe void MiMetodo () // Método no seguro


{ ... }

unsafe class MiClase // Clase (struct) no segura


{ ... } // Todos los miembros son no seguros

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];

for (int i=0; i<TAM; i++) pt[i] = i;

for(int i=0; i<TAM; i++)


System.Console.WriteLine(pt[i]);

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:

 public static void Assert(bool) comprueba una condición y envía la


pila de llamadas si ésta es falsa.
 public static void Assert(bool, string) Comprueba una condición
y muestra un mensaje si ésta es falsa.
 ublic static void Assert(bool, string, string Comprueba una
condición y muestra ambos mensajes si es false.

Compilación condicional: Aserciones

public class Debug


{
public static void Assert(bool cond, String s)
{
if (!cond) {
throw new AssertionException(s);
}
}

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.

csc programa.cs /doc:docum_programa.xml

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.

Etiqueta XML Descripción


<summary> Descripción breve de tipos y miembros.
<remarks> Descripción detallada de tipos y miembros.
<para> Delimita párrafos.
<example> Ejemplo de uso.
<see>
Referencias cruzadas. Usa el atributo cref
<seealso>
<c> <code> Código de ejemplo (verbatim).

<param> Parámetros de métodos. Usa el atributo name.


Referencia a parámetros de metodos. Usa el
<paramref>
atributo name.
<returns> Valor devuelto por el método.

<exception> Descripciçon de Excepciones.


<value> Descripción de propiedades.

<list> Generar listas. Usa el atriibuto (opcional) type.

Generar listas. Usa el atriibuto (opcional) type


<item>
(puede ser: bullet, number o table).

<permission> Permisos.

Veamos un ejemplo detallado:

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>

public readonly uint X;

/// <summary>
/// Campo que contiene la coordenada Y de un punto
/// </summary>
/// <remarks>
/// Es de solo lectura
/// </remarks>

public readonly uint Y;

/// <summary>
/// Constructor de la clase
/// </summary>
/// <param name="x">Coordenada x</param>
/// <param name="y">Coordenada y</param>

public Punto(uint x, uint y) {


this.X=x;
this.Y=y;
}

} // fin de class Punto


/// <summary>
/// Clase Cuadrado. Los objetos de esta clase son polígonos
/// cerrados de cuatro lados de igual longitud y que
/// forman ángulos rectos.
/// </summary>
/// <remarks>
/// Los cuatro vértices pueden numerarse de manera que
/// el vértice 1 es el que tiene los menores valores de
/// las coordenadas X e Y.
///
/// Los demás vértices se numeran a partir de éste recorriendo
/// el cuadrado en sentido antihorario.
/// </remarks>

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;
}
}

......

} // Fin de class Cuadrado

} // Fin de namespace Geometria

namespace PruebaGeometria {
using Geometria;
class GeometriaApp {
....

Para generar la documentación en Visual Studio .NET seleccionaremos el proyecto en el


explorador de soluciones y daremos el nombre del fichero XML que contendrá la
documentación: Ver | Páginas de propiedades | Propiedades de configuración |
Generar | Archivo de documentación XML y darle el nombre:
DocumentacionGeometia, por ejemplo.
Para ver el resultado: Herramientas | Generar páginas Web de comentarios. Unos
ejemplos:
Conceptos básicos de programación
orientada a objetos

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

 System.Object.Equals(Object o) comprueba si dos objetos son iguales.


 System.Object.ReferenceEquals(Object o) comprueba si dos referencias
apuntan al mismo objeto.
 System.Object.Finalize() es invocado por el recolector de basura para liberar
el objeto.
 System.Object.GetHashCode() se emplea en la implementación colecciones de
objetos (del tipo System.Collections.HashTable) y debe implementarse en las
subclases de System.Object para crear buenas funciones hash (por defecto, se utiliza la
identidad del objeto).
 System.Object.GetType() devuelve el tipo de un objeto (punto de entrada para
el sistema de reflexión de la plataforma .NET).
 System.Object.MemberwiseClone() crea un clon del objeto utilizando la
capacidad de reflexión de la plataforma .NET
 System.Object.ToString() devuelve una representación textual del objeto. Por
defecto, devuelve el nombre de la clase, por lo que deberemos implementarlo nosotros.
No está diseñado para mostrar mensajes al usuario (para esa función se debería utilizar el
interfaz IFormattable)

public class Person


{
String name;

public override string ToString()


{
return name;
}
}

...

Name n1 = new Name("Fred");


Name n2 = new Name("Fred");
Name n3 = n2; // n2 & n3 apuntan al mismo objeto

if (n1 == n2) ... // false


if (n2 == n3) ... // true
if (n1.Equals(n2)) ... // true
if (n2.Equals(n3)) ... // true
Tipos primitivos

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:

 C#: bool, int, long, string, double, float...


 Visual Basic.NET: Boolean, Integer, String...

Algunos tipos primitivos de interés son: System.Byte, System.Char, System.Boolean


(valores lógicos: true o false), System.Guid (identificador universal de 128 bits, que
además incluye un generador System.Guid.NewGuid()) y los valores nulos
(System.DBNull para valores NULL en bases de datos, System.Empty para representar el
valor VT_EMPTY en COM y System.Missing para trabajar con parámetros opcionales).

Cadenas de caracteres

La clase System.String es la utilizada en todos los lenguajes de la plataforma .NET para


representar cadenas de caracteres, que se almacenan utilizando UNICODE. El tipo
System.String es inmutable, por lo que los métodos que parecen modificar una cadena en
realidad lo que hacen es crear una nueva. La inmutabilidad de los objetos de este tipo hace
recomendable el uso de String.Format o StringBuilder para manipular cadenas de
caracteres (como en Java).

La clase String incluye métodos para:

 Realizar búsquedas en cadenas de caracteres: IndexOf(), LastIndexOf(),


 StartsWith(), EndsWith()
 Eliminar e insertar espacios en blanco: Trim(), PadLeft(),
 PadRight()
 Manipular subcadenas: Insert(), Remove(), Replace(), Substring(),
 Join(), Split()
 Modificar cadenas de caracteres: ToLower(), ToUpper(), Format() (al estilo del
printf de C, pero seguro).

Comparación de cadenas

public static int Main()


{
string s = "abc";
string s1 = s; // s1 y s hacen referencia al mismo objeto
string s2 = "abc";

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

La clase System.Console proporciona la funcionalidad básica de E/S (equivalente a stdin,


stdout y stderr en C). Para escribir se puede utilizar Write() o WriteLine() empleando la
sintaxis de String.Format, mientras Read() lee un caracter y ReadLine() lee una línea
completa.

Console.Write("Snow White and the {0} dwarfs", 7);

Clases útiles de System

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);
}

El método Format da formato al valor de la instancia actual tal como se le especifique:

String.Format("Please order {0} widgets at {1} each.", i,


f);
String.Format("{0:U}", DateTime.Now);

El parámetro IServiceProvider se puede emplear para obtener características especiales de


la configuración local (p.ej. delimitadores para números y fechas).

Incluso se puede implementar el interfaz ICustomFormatter para redefinir el formato en el


que se muestran los valores de los tipos predefinidos.

IDisposable

Este interfaz nos permite controlar explícitamente la liberación de recursos:


class ResourceWrapper : IDisposable
{
private IntPrt handle; // Puntero a un recurso externp
private OtherResource otherRes;

bool disposed = false;

private void free ()


{
if (!disposed) {
CloseHandle (handle);
dispose = true;
}
}

public void Dispose


{
free();
OtherRes.Dispose();
GC.Suppress.Finalization(this);
}

public void Finalize ()


{
free();
Base.Finalize();
}
}
Colecciones
Arrays

Los arrays, representados mediante la clase System.Array, constituyen la única colección de


datos que queda fuera del espacio de nombres System.Collections. La clase
System.Array corresponde a los arrays en cualquier lenguaje de programación de la
plataforma .NET y sirven para almacenar objetos de forma polimórfica (esto es, sirven para
almacenar instancias de cualquier clase que herede de System.Object [todas]).

Los arrays pueden tener un número arbitrario de dimensiones y su tamaño se especifica al


crearlos (CreateInstance). Una vez creado el array, su tamaño es fijo.

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.

public static void Main()


{
// Creación e inicialización arrays
int[] intArray = new int[5] { 1, 2, 3, 4, 5 };
Object[] objArray = new Object[5] { 26, 27, 28, 29, 30 };

// Copia de los dos primeros elementos de intArray en


objArray
Array.Copy( intArray, objArray, 2 );

// Volcado en consola de los valores actuales de los


arrays
Console.Write( "intArray: " ); PrintValues( intArray );
Console.Write( "objArray: " ); PrintValues( objArray );

// Copia de los dos últimos elementos de objArray en


intArray
Array.Copy( objArray, objArray.GetUpperBound(0) - 1,
intArray, intArray.GetUpperBound(0) - 1, 2 );
}

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).

IEnumerable & IEnumerator

El interfaz System.Collections.IEnumerable permite iterar sobre una colección de datos,


proporcionando un mecanismo estándar para todas las colecciones:

public interface IEnumerable


{
IEnumerator GetEnumerator();
}
El métodoGetEnumerator() devuelve un iterador que implementa el interfaz
System.Collections.IEnumerator:

public interface IEnumerator


{
Boolean MoveNext();
Object Current { get; }
void Reset();
}

SetType st = new SetType(...);

// Se obtiene un iterador para enumerar los elementos de la


colección

IEnumerator e = st.GetEnumerator();

// Se recorre la colección utilizando el iterador como


cursor

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

El interfaz System.Collections.ICollection se deriva de IEnumerable y proporciona


un interfaz básico para todas las colecciones: Count(), CopyTo(), IsSynchronized()

IList

El interfaz System.Collections.IList también deriva de ICollection y permite trabajar


con listas. Proporciona un indexador Item para acceder a los elementos de la lista en función
de su posición y una serie de métodos para trabajar con el contenido de la lista: Add(),
Remove(), Contains(), Clear()

IDictionary

El interfaz System.Collections.IDictionary especializa el interfaz ICollection y se


utiliza para implementar diccionarios (conjuntos de pares clave-valor, a modo de memoria
asociativa). Este interfaz proporciona un indexador Item para consultar un valor dada su clave
y los mismos métodos que IList para trabajar con el diccionario: Add(), Remove(),
Contains(), Clear() .

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

La clase System.Collections.ArrayList implementa el interfaz IList para proporcionar


arrays dinámicos cuyo tamaño puede variar dinámicamente (a diferencia de System.Array,
que mantiene fijo su tamaño una vez que ha sido creado).

using System;
using System.Collections;

public class SampleArrayList


{
public static void Main()
{
// Creación
ArrayList myAL = new ArrayList();

// 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 );
}

public static void PrintValues( IEnumerable myList )


{
IEnumerator myEnumerator = myList.GetEnumerator();

while ( myEnumerator.MoveNext() )
Console.Write("\t{0}", myEnumerator.Current );

Console.WriteLine();
}
}

BitArray

La clase System.Collections.BitArray proporciona una implementación eficiente y


compacta de arrays de bits.

HashTable

La clase System.Collections.HashTable implementa un diccionario (IDictionary)


mediante una tabla hash.

SortedList

La clase System.Collections.SortedList implementa una lista ordenada sin duplicados


que puede indexada por enteros (posición) y por cadenas (contenido).
Stack

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;

public class SampleStack


{
public static void Main()
{
Stack myStack = new Stack();

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 );
}

public static void PrintValues( IEnumerable myCollection )


{
IEnumerator myEnumerator = myCollection.GetEnumerator();

while ( myEnumerator.MoveNext() )
Console.Write( "\t{0}", myEnumerator.Current );

Console.WriteLine();
}

public static void EmptyStack( Stack myStack )


{
object value = myStack.Pop();

while (value!=null) {
Console.Write ("\t{0}", value);
value = myStack.Pop();
}

Console.WriteLine();
}
}

Queue

La clase System.Collections.Queue implementa una cola FIFO con sus métodos


Enqueue() y Dequeue(). Igual que sucede con las pilas, las colas también son enumerables:

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);
}

public static void PrintValues ( Ienumerable myCollection )


{
IEnumerator myEnumerator = myCollection.GetEnumerator();

while ( myEnumerator.MoveNext() )
Console.Write( "\t{0}", myEnumerator.Current );

Console.WriteLine();
}

public static void EmptyQueue( Queue myQ )


{
object value = myQ.Dequeue();

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:

 System.IO.Directory y System.IO.File proporcionan métodos estáticos para


manipular directorios y ficheros, respectivamente.
 System.IO.DirectoryInfo Y System.IO.FileInfo incluyen métodos para
manipular instancias de directorios y ficheros, respectivamente.
System.IO.DirectoryInfo representa un directorio concreto, a partir del cual se
pueden obtener sus subdirectorios (con GetDirectories([mask])) y los ficheros que
incluye (con GetFiles([mask]). Por su parte, System.IO.FileInfo representa un
fichero concreto, que se puede obtener directamente especificando su path o a partir de la
enumeración de ficheros de un directorio con GetFiles().

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

La clase base de los streams en la plataforma .NET es la clase abstracta System.IO.Stream,


que proporciona funciones de acceso síncrono (Read(), Write()) y asíncrono
(BeginRead(),

BeginWrite(), EndRead(), EndWrite()).

 System.IO.FileStream se utiliza para acceder directamente al contenido de


ficheros. De hecho es el tipo devuelto por una llamada a File.Open().
 System.IO.MemoryStream permite construir streams en memoria.

Lectura

Los stream readers proporcionan acceso de lectura a un stream:

 System.IO.BinaryReader permite leer datos en binario: ReadInt16(),


ReadBoolean(), ReadDouble(), etc.
 System.IO.TextReader es una clase abstracta utilizada como base común de las
dos siguientes:
 System.IO.StreamReader hereda de TextReader e implementa los métodos
ReadLine() para leer una línea de texto y ReadToEnd() para leer lo que quede de
stream.
 System.IO.StringReader también hereda de TextReader y se utiliza para
simular streams a partir de cadenas de caracteres.

Lectura de un fichero de texto

static void Main(string[] args)


{
string filename = @"../../AssemblyInfo.cs";
string line;

Console.WriteLine ("*****************************");

StreamReader stream = new StreamReader(filename);


// StreamReader stream = File.OpenText (filename);

do {
line = stream.ReadLine();
Console.WriteLine (line);
} while (line != null);

stream.Close();

Console.WriteLine ("*****************************");

Console.ReadLine();
}

Escritura

Los stream writers sirven para escribir en streams:

 System.IO.BinaryWriter nos permite escribir datos en binario, mediante la


utilización del método sobrecargado Write().
 System.IO.TextWriter es la clase abstracta que sirve de base para
StreamWriter y StringWriter.
 System.IO.StreamWriter hereda de TextWriter y sirve para escribir cadenas de
texto en streams.
 System.IO.StringWriter también hereda de TextWriter y simula streams en
cadenas de caracteres.

public class MyWriter


{
private Stream stream;

public MyWriter(Stream stream)


{
this.stream = stream;
}

// Almacena la representación binaria de un double

public void WriteDouble(double myData)


{
byte[] b = myData.GetBytes();
stream.Write(b,0,b.Length);
}

public void Close()


{
stream.Close();
}
}

static void Main(string[] args)


{
string filenameI = @"../../AssemblyInfo.cs";
string filenameO = @"../../COPIA_AssemblyInfo.cs";
string line;

StreamReader streamI = new StreamReader(filenameI);


StreamWriter streamO = new StreamWriter(filenameO);

while ((line = streamI.ReadLine()) != null) {


streamO.WriteLine (line);
}

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

La clase abstracta System.Net.WebRequest es la clase base para el uso de protocolos de red


como HTTP. Esta clase sirve de punto de acceso común para distintos protocolos. Además,
permite registrar nuevos protocolos mediante el método RegisterPrefix().

La clase HttpWebRequest da soporte a los protocolos HTTP y HTTPS. El contenido de la


solicitud HTTP[S] se rellena con un stream ( WebRequest.GetRequestStream()) y la
solicitud se ejecuta con GetResponse(). Los datos devueltos tras ejecutar GetResponse()
se pueden leer mediante el método WebResponse.GetReponseStream().

HttpWebRequest HttpWReq =
(HttpWebRequest)
WebRequestFactory.Create("http://elvex.ugr.es");

HttpWebResponse HttpWResp =
(HttpWebResponse)HttpWReq.GetResponse();

SMTP

MailMessage MyMessage = new MailMessage();


MyMessage.To = "[email protected]";
MyMessage.From = "MyApplication";
MyMessage.Subject = "Unhandled Error!!!";
MyMessage.BodyFormat = MailFormat.Html;
MyMessage.Body = "<html><body><h1> ERROR
</h1></body></html>";
SmtpMail.Send(MyMessage);

Serialización

La serialización es el proceso de convertir un objeto, o un grafo conexo de objetos, en una


secuencia de bytes.
using System;
using System.IO;
using System.Collections;
using System.Serialization;
using System.Serialization.Formatters.Binary;

class SerializeExample
{
public static void Main(String[] args)
{
ArrayList l = new ArrayList();

for (int x=0; x < 100; x++)


l.Add (x);

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();

ArrayList p = (ArrayList) b.Deserialize(s);


p.ToString();
}
}

Un tipo no es serializable salvo que se marque específicamente con el atributo


Serializable:

[Serializable] public class MyClass {}

[Serializable] public class MyClass


{
[NotSerialized] int _size;
}

El proceso de serialización puede personalizarse si implementamos el interfaz


ISerializable, el interfaz IDeserializationEventListener o creamos nuestros propios
"formateadores" (custom formatters).
ISerializable
Un único método:
void GetObjectData (SerializationInfo info,
StreamingContext context);
y un constructor que puede ser privado:
private <T> (SerializationInfo info,
StreamingContext context)
IFormatter

public interface IFormatter


{
// Propiedades
SerializationBinder Binder { get; set; }
StreamingContext Context { get; set; }
ISurrogateSelector SurrogateSelector { get; set; }

// 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.

Bases de datos relacionales

 Tabla o relación: Colección de registros acerca de entidades de un tipo específico


(p.ej. alumnos).
 Atributo, campo o columna: Propiedad asociada a una entidad (p.ej. nombre,
apellidos...). Cada atributo tiene un tipo asociado (p.ej. entero, cadena de caracteres...) y
puede tomar el valor nulo (null).
 Tupla, registro o fila: Datos relativos a un objeto distinguible de otros (p.ej. un
alumno concreto).

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).

 Clave primaria: Conjunto de atributos que nos permiten identificar unívocamente a


una entidad dentro de un conjunto de entidades (p.ej. número de matrícula). La clave
primaria garantiza la unicidad de una tupla, pues no se permite la existencia de varios
registros que compartan su clave primaria.
 Clave externa: Conjunto de atributos que hacen referencia a otra tabla, lo que nos
permite establecer relaciones lógicas entre distintas tablas. Los valores de los atributos de
la clave externa han de coincidir con los valores de los atributos de una clave (usualmente
la primaria) en una de las tuplas de la tabla a la que se hace referencia (integridad
referencial).
SQL

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:

 DDL (Data Definition Language), utilizado para crear y modificar la estructura de la


base de datos (p.ej. CREATE TABLE).
 DML (Data Manipulation Language), empleado para manipular los datos almacenados
en la base de datos (p.ej. consultas con la sentencia SELECT).
 DCL (Data Control Language), para establecer permisos de acceso (GRANT, REVOKE,
DENY) y gestionar transacciones (COMMIT y ROLLBACK).
Interfaces de acceso a bases de datos
Evolución histórica de los "estándares" propuestos por Microsoft:

 ODBC (Open Database Connectivity): API estándar ampliamente utilizado,


disponible para múltiples DBMSs, utiliza SQL para acceder a los datos.
 DAO (Data Access Objects): Interfaz para programar con bases de datos
JET/ISAM, utiliza automatización OLE y ActiveX.
 RDO (Remote Data Objects): Fuertemente acoplado a ODBC, orientado al
desarrollo de aplicaciones cliente/servidor.
 OLE DB: Construido sobre COM, permite acceder a bases de datos tanto relacionales
como no relacionales (no está restringido a SQL). Se puede emplear con controladores
ODBC y proporciona un interfaz a bajo nivel en C++.
 ADO (ActiveX Data Objects): Ofrece un interfaz orientado a objetos y proporciona
un modelo de programación para OLE DB accesible desde lenguajes distintos a C++ (p.ej.
Visual Basic).

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:

 Acceso conectado: Acceso sólo de lectura con cursores unidireccionales ("firehose


cursors"). La aplicación realiza una consulta y lee los datos conforme los va procesando
con la ayuda de un objeto DataReader.
 Acceso desconectado: La aplicación ejecuta la consulta y almacena los resultados de
la misma para procesarlos después accediendo a un objeto de tipo DataSet. De esta
forma, se minimiza el tiempo que permanece abierta la conexión con la base de datos.

Al proporcionar conjuntos de datos de forma desconectada, se utilizan mejor los recursos de


los servidores y se pueden construir sisyemas más escalables que con ADO (que mantenía
abierta la conexión con la base de datos la mayor parte del tiempo). Este enfoque resulta más
adecuado en sistemas distribuidos como Internet.
Arquitectura ADO.NET
El funcionamiento de ADO.NET se basa esencialmente en utilizar los siguientes componentes:

 Data Provider (proveedor de datos): Proporciona un acceso uniforme a conjuntos


de datos (bases de datos relacionales o información ID3 de ficheros MP3). Su papel el
similar al de un controlador ODBC o JDBC.
 DataSet: El componente más importante, puede almacenar datos provenientes de
múltiples consultas (esto es, múltiples tablas).
 DataAdapter: Sirve de enlace entre el contenedor de conjuntos de datos (DataSet)
y la base de datos (Data Provider).

Los componentes anteriores se completan con DataReader (para realizae eficientemente


lecturas de grandes cantidades de datos que no caben en memoria), DataRelation (la forma
de establecer una reunión entre dos tablas), Connection (utilizada por DataAdapter para
conectarse a la base de datos) y Command (que permite especificar las órdenes,
generalmente en SQL, que nos permiten consultar y modificar el contenido de la base de
datos: select, insert, delete y update).

Un proveedor de datos debe proporcionar una implementación de Connection, Command,


DataAdapter y DataReader.

El modo de funcionamiento típico de ADO.NET es el siguiente:

 Se crean un objeto Connection especificando la cadena de conexión.


 Se crea un DataAdapter.
 Se crea un objeto Command asociado al DataAdapter, con la conexión adecuada y la
sentencia SQL que haya de ejecutarse.
 Se crea un DataSet donde almacenar los datos.
 Se abre la conexión.
 Se rellena el DataSet con datos a través del DataAdapter.
 Se cierra la conexión.
 Se trabaja con los datos almacenados en el DataSet.

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.

IDbCommand nos permite definir la sentencia que se ha de ejecutar, ejecutar la sentencia,


pasarle parámetros y prepararla (crear una versión "compilada" de la misma para que su
ejecución sea más eficiente cuando ha de repetirse varias veces). El método ExecuteReader
devuelve un conjunto de tuplas (véase el interfaz IDataReader), mientras que
ExecuteScalar devuelve un valor único (p.ej. ejecución de procedimientos almacenados) y
ExecuteNonQuery no devuelve nada (p.ej. borrados y actualizaciones).

string connectionString = "Persist Security Info=False;" +


"User ID=sa;Initial Catalog=MYDB;" +
"Data Source=MYSERVER";

SqlConnection connection = new SqlConnection(connectionString);

// Ejecución de sentencias SQL


// ---------------------------

string sqlInsert = "INSERT INTO Department(DepartmentName) VALUES


(@DepartmentName)";

SqlCommand insertCommand = new SqlCommand(sqlInsert, connection);

SqlParameter param = insertCommand.Parameters.Add (


new SqlParameter("@DepartmentName",
SqlDbType.VarChar, 100));

param.Value = ...

connection.Open();

insertCommand.ExecuteNonQuery();

connection.Close();

// Llamadas a procedimientos almacenados


// -------------------------------------
// C#

string spName = "CREATE_DEPARTMENT"

SqlCommand command = new SqlCommand(spName, connection);

command.CommandType = CommandType.StoredProcedure;

SqlParameter in = command.Parameters.Add (
new SqlParameter("@DepartmentName", SqlDbType.VarChar,
100));
in.Value = ...

SqlParameter out = command.Parameters.Add (


new SqlParameter("RETVAL", SqlDbType.Int));
out.Direction = ParameterDirection.ReturnValue;

connection.Open();

insertCommand.ExecuteNonQuery();

int newID = command.Parameters("RETVAL").Value;

connection.Close();

// SQL Server
// ----------

CREATE TABLE [dbo].[Department] (


[DepartmentID] [int] IDENTITY (1, 1) NOT NULL ,
[DepartmentName] [varchar] (100),
[CreationDate] [datetime] NULL
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[Department] WITH NOCHECK ADD


CONSTRAINT [PK_Department] PRIMARY KEY CLUSTERED
(
[DepartmentID]
) ON [PRIMARY]
GO

CREATE PROCEDURE dbo.CreateDepartment


@DepartmentName varchar(100),
AS
INSERT INTO Department (DepartmentName, CreationDate)
VALUES (@DepartmentName, GetDate())
RETURN scope_identity()
GO

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";

OleDbConnection connection = new OleDbConnection(connectionString);

string sqlQuery = "SELECT CompanyName FROM Customers";

OleDbCommand myCommand = new OleDbCommand(sqlQuery, connection);


connection.Open();
OleDbDataReader myReader = myCommand.ExecuteReader();

while (myReader.Read()) {
Console.WriteLine(myReader.GetString(0));
}

myReader.Close();

connection.Close();

Clase DataSet

Un objeto DataSet encapsula un conjunto de tablas independientemente de su procedencia y


mantiene las relaciones existentes entre las tablas. El contenido de un DataSet puede
serializarse en formato XML. Además, se permite la modificación dinámica de los datos y
metadatos del conjunto de datos representado por el objeto DataSet.

El interfaz IDataAdapter implementado OleDbDataAdapter y SqlDataAdapter se utiliza


para construir el conjunto de datos y actualizarlo cuando sea necesario. Los conjuntos de datos
con los que se trabaja de esta forma utilizan un enfoque asíncrono en el que no se mantiene
abierta la conexión con la base de datos a la que se está accediendo. Al trabajar con conjuntos
de datos de esta forma, se dispone de un superconjunto de los comandos que se permiten
cuando se emplea el interfaz IDataReader. De hecho se pueden realizar operaciones de
consulta (select), inserción (insert), actualización (update) y borrado (delete).

string connectionString = "Persist Security Info=False;" +


"User ID=sa;Initial Catalog=Northwind;" +
"Data Source=MYSERVER";

SqlConnection connection = new SqlConnection(connectionString);

SqlDataAdapter myDataAdapter = new SqlDataAdapter();

DataSet myDataSet = new DataSet();

string sqlQuery = "SELECT * FROM Customers";

myDataAdapter.SelectCommand = new SqlCommand(sqlQuery, connection);

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

Representa los datos de una tabla (almacenados en la colección Rows de un objeto


DataTable), de acuerdo con el esquema definido por las columnas de la tabla (Columns).
Además, incluye propiedades para determinar el estado de una fila/tupla particular (p.ej.
nuevo, cambiado, borrado, etc.).

Clase DataRelation

Relaciona dos DataTables vía DataColumns y sirve para mantener restricciones de


integridad referencial. Obviamente, el tipo de las columnas relacionadas ha de ser idéntico.
Para acceder a los datos relacionados con un registro concreto basta con emplear el método
GetChildRecords de la tupla correspondiente (DataRow).

Creación de una base de datos

// Creación de las tablas, columnas y claves primarias/externas

DataTable authors = new DataTable("Author");


DataTable books = new DataTable("Book");

DataColumn id = authors.Columns.Add("ID", typeof(Int32));


DataColumn name = new authors.Columns.Add("Name",typeof(String));
authors.PrimaryKey = new DataColumn[] {id};
id.AutoIncrement = true;

DataColumn isbn = books.Columns.Add("ISBN", typeof(String));


DataColumn title = books.Columns.Add("Title", typeof(String));
DataColumn authid = books.Columns.Add(“AuthID”,typeof(Int32));
books.PrimaryKey = new DataColumn[] {isbn};

DataColumn[] foreignkey = new DataColumn[] {authid};


DataRelation bookauth = new DataRelation("BookAuthors",
authors.PrimaryKey, foreignkey);

// Creación del DataSet: tablas y relaciones

DataSet dataset = new DataSet();


dataset.DataSetName = "BookAuthors";
dataset.Tables.Add (authors);
dataset.Tables.Add (books);

dataset.Relations.Add (bookauth);

// Inserción de datos

DataRow shkspr = authors.NewRow();


shkspr["Name"] = "William Shakespeare";
authors.Rows.Add(shkspr);

DataRow row = books.NewRow();


row["AuthID"] = shkspr["ID"];
row["ISBN"] = "1000-XYZ";
row["Title"] = "MacBeth";
books.Rows.Add(row);

// 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).

Data Binding en Visual Studio .NET


Se puede optar por asistentes del tipo de DataForm wizard... o, cuando éstos no nos
ofrecen la funcionalidad suficiente para nuestras aplicaciones, podemos programarlo
nosotros mismos (algo que no es difícil y nos da bastante más control sobre nuestra
aplicación):

 Se crean los objetos ADO.NET necesarios (Connection, DataSet, Command y


DataAdapter).
 Se enlazan los controles de la interfaz con las columnas del DataSet, lo que
se consigue añadiendo un objeto System.Windows.Forms.Binding a la
propiedad DataBindings del control. Los objetos de tipo Binding nos permiten
enlazar una propiedad de un control a una columna de un DataSet.
 Se implementa la forma de recorrer los registros, haciendo uso del objeto
BindingContext asociado al formulario (cuyas propiedades Position y Count
nos permiten movernos por el conjunto de datos).
 Se implementan las operaciones sobre la base de datos (con los comandos
asociados al DataAdapter.

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.

XML, al que algunos consideran el Esperanto de los sistemas de información, se emplea


principalmente para representar documentos (texto con etiquetas que identifican porciones del
documento, como es el caso de estos apuntes) y conjuntos de datos (estructuras de datos
jerárquicas, para ser más precisos).

Entre sus ventajas se encuentra su aceptación casi universal, su legibilidad y su carácter


autocontenido (a diferencia de los formatos binarios propios de cada aplicación), si bien el
tamaño de los documentos XML es mayor que el de sus equivalentes binarios y su
procesamiento requiere más recursos, por lo que no resulta adecuado en aplicaciones en las
que la eficiencia sea un objetivo prioritario.

En resumen, XML permite representar datos de forma homogénea en entornos heterogéneos,


lo que facilita la interoperabilidad entre distintos sistemas. Además, hoy en día goza de gran
popularidad, pues todas las grandes empresas promueven su utilización y se han desarrollado
numerosas tecnologías basadas en este formato estándar (SOAP, ebXML...).
Un pequeño ejemplo

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"

Representación de los mismos datos en XML:

<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> ... </tag_name>

También pueden estar vacíos (elementos sin contenido):

<tag_name />

La primera línea es correcta en HTML, no en XML. La segunda sí lo es:

<LI>Esto es HTML<BR> que es muy permisivo.</LI>

<LI>XML es <BR/> mucho más restrictivo.</LI>

El contenido de un elemento puede ser complejo: Un elemento puede contener a otros


elementos.

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 ('):

<tag_name attribute="value"> ... </tag_name>

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:

 UTF-8 (8-bit UCS/Unicode Transformation Format) coincide con ASCII a la


hora de representar caracteres del estándar americano, mientras que
emplea 2 bytes para representar letras latinas acentuadas (vocales con
tilde o la eñe, en el caso del castellano) y otros símbolos:

<?xml version="1.0" encoding="utf-8"?>

 ISO-8859-1 es el estándar de codificación, también conocido como Latin-1,


que se utiliza para transmitir documentos vía HTTP de tipo MIME "text/...":

<?xml version="1.0" encoding="ISO-8859-1"?>

 La codificación Windows-1252 utilizada por defecto en Windows coincide con


la ISO-8859-1 salvo para los caracteres 0x80 - 0x9F, que Windows-1252
interpreta como letras y signos de puntuación mientras que el estándar
ISO-8859-1 los procesa como caracteres de control
(http://en.wikipedia.org/wiki/ISO-8859-1)

Debajo se muestra un ejemplo más detallado en el que, aparte de indicar la codificación


empleada, se especifica la hoja de estilo que se ha de aplicar al documento para su
presentación:

<?xml version="1.0" encoding="utf-8"?>


<?xml-stylesheet type="text/xsl" href="template.xsl"?>

<root>
<element1>
<subelement1 />
<subelement2 />
</element1>
<element2>...</element2>
<element3>...</element3>
<element4>...</element4>
</root>
Entidades

Las entidades en XML proporcionan un mecanismo de substitución textual:

Entidad Sustitución
&lt; <
&gt; >
&amp; &
&apos; '
&quot; "

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 "&#38;#60;">
<!ENTITY gt "&#62;">
<!ENTITY amp "&#38;#38;">
<!ENTITY apos "&#39;">
<!ENTITY quot "&#34;">

Comentarios y secciones CDATA

Finalmente, un documento XML también puede incluir comentarios y secciones CDATA:

 Comentarios, delimitados por las construcciones <!-- y --> (exactamente


igual que en HTML) y

<!-- Esto es un comentario en XML -->

 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).

<!-- Este documento XML no usa CDATA -->

<ejemplo>
&lt;HTML&gt;
&lt;HEAD&gt;
&lt;TITLE&gt;Rock &amp; Roll&lt;/TITLE&gt;
&lt;/HEAD&gt;
&lt;/HTML&gt;
</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:

<?xml version="1.0" ?>


<parent>
<child1> 1 </child1>
<child2> <child3> ¿2 ó 3? </child2> </child3>
</parent>

El siguiente documento XML tampoco es válido porque tiene dos raíces:

<?xml version="1.0" ?>


<parent>
<child> ... </child>
</parent>
<parent>
<child> ... </child>
</parent>

Sin embargo, el siguiente documento XML sí que es válido:

<?xml version="1.0" ?>


<parent>
<child1> ... </child1>
<child2 />
<child3> ... </child3>
</parent>
En la siguiente figura se muestra un ejemplo de un documento XML bien formado donde se
distinguen claramente sus distintas partes:

Una versión "castellanizada" del mismo podría tener el siguiente aspecto:

Un documento XML bien formado

<?xml version='1.0' encoding="utf-8"?>


<libreria>
<libro genero='Poesía' fechaPublicacion='1932'
ISBN='1-861003-11-0'>
<titulo>Poeta en Nueva York</titulo>
<autor>
<nombre>Federico</nombre>
<apellidos>García Lorca</apellidos>
</autor>
<precio>8.99</precio>
</libro>
<libro genero='Novela' fechaPublicacion='1967'
ISBN='0-201-63361-2'>
<titulo>The Confidence Man</titulo>
<autor>
<nombre>Herman</nombre>
<apellidos>Melville</apellidos>
</autor>
<precio>11.99</precio>
</libro>
</libreria>

Un documento XML bien formado


Espacios de nombres
Los espacios de nombres (namespaces) permiten que XML sea extensible, permitiendo
diferenciar entre etiquetas con el mismo nombre utilizando prefijos. De esta forma se pueden
evitar conflictos de nombres y el que diseña un documento XML puede centrarse en los datos
que tiene y cómo describirlos mejor. Además, el uso de identificadores universales URI
(Uniform Resource Identifier) para hacer referencia a estándares ampliamente aceptados
permite que se puedan combinar documentos escritos independientemente.

Sintaxis

xmlns:prefijo = URI
En la declaración anterior:

 URI es el identificador universal del espacio de nombres, su nombre real


(cuando se usa una URL, ésta no tiene porqué hacer referencia a un
servidor activo).
 prefijo es el identificador por el que nos referiremos a los distintos elementos
del espacio de nombres en el documento actual, usándolo como prefijo.

Ejemplos de declaración de namespaces

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>

Una colección de libros.

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>

Una colección de discos.

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>

Una colección de libros y discos.


Espacios de nombres por defecto

Un espacio de nombres XML declarado sin prefijo se convierte automáticamente en el espacio


de nombres por defecto para todos los subelementos del elemento en el que aparece la
declaración.

Ejemplo de uso de namespaces por defecto

<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).

En el siguiente ejemplo, el espacio de nombres por defecto es


http://csharp.ikor.org/libros:

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>

Una colección de libros y discos.

Ámbito de los espacios de nombres

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):

Ámbito de los namespaces

<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>

Espacios de nombres y atributos

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

¿Para qué sirven los esquemas XML?


XML es un formato abierto muy útil para enviar información de un sitio a otro. En ocasiones,
sin embargo, la flexibilidad de XML puede resultar contraproducente. ¿Cómo sabemos que los
datos contenidos en un documento XML son consistentes con los que esperábamos recibir? Los
esquemas XML nos permiten verificarlo.

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.

Los esquemas XML tienen, pues, dos cometidos fundamentales:

 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.

Desde su creación, se han propuesto distintos estándares para la especificación de esquemas


XML, siendo DTD y XSD los más utilizados. En la actualidad, DTD (Document Type Definition)
está cayendo en desuso, ya que los esquemas XSD (XML Schema Definition) se definen
utilizando documentos XML, mientras que DTD utiliza un formato que no es XML.

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.

Nociones sobre DTDs.

Para utilizar una DTD como mecanismo de restricción, se especifica lo siguiente en el


documento 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.

<!DOCTYPE elem_raiz PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"


"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
DTD para un ejemplo que contenga elementos de tipo libro:

<?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.

Modificadores de número de ocurrencias:

 ?: Una vez o ninguna


 +: Al menos una vez
 *: Cualquier número de veces o ninguna
 (nada): Exactamente una vez
 Para opciones alternativas: separar con |.

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:

<?xml version="1.0" encoding="utf-8"?>


<xsd:schema id="stock"
xmlns:xsd="http://www.w3c.org/2001/XMLSchema"
targetNamespace="http://elvex.ugr.es/stock.xsd">

<xsd:complexType>

<xsd:sequence>

<xsd:element name="ID" type="xsd:unsignedInt" />


<xsd:element name="description" type="xsd:string" />
<xsd:element name="price" type="xsd:decimal" />
<xsd:element name="quantity" type="xsd:integer" />

</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).

A continuación, la etiqueta xsd:complexType nos permite definir tipos de forma similar a


como se especifican los tipos definidos por el usuario en un lenguaje de programación,
indicando los elementos correspondientes a cada dato almacenado acerca de los productos de
nuestro almacén, su identificador y su tipo.
Elementos
Un elemento (etiqueta xsd:element) se utiliza para describir datos.

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:

Tipo XSD Tipo .NET


anyType object
Boolean bool
Byte sbyte
date | dateTime | time DateTime
decimal decimal
double double
duration Timespan
float single
ID | Name string
int Int32
integer | long Int64
short Int16
string string
unsignedByte Byte
unsignedInt UInt32
unsignedLong UInt64
unsignedShort UInt16

Además de poder indicar su nombre y su tipo, podemos especificar restricciones adicionales


para los elementos de un documento XML.minOccurs y maxOccurs nos permiten especificar
el número mínimo y el número máximo de veces que un elemento puede aparecer en el
documento (por defecto, 1).
Atributos
Los atributos son similares a los elementos, si bien un atributo ha de ser de un tipo simple y
tiene declararse justo antes de cerrar la etiqueta xsd:complexType. A diferencia de los
elementos, los atributos pueden aparecer en cualquier orden y no pueden incluir otros
elementos (al ser de tipos simples). No obstante, su característica más interesante es que
pueden ser opcionales y se les puede asignar un valor por defecto:

<xsd:attribute name="rebate" type="xsd:decimal" />

El atributo use de xsd:attribute puede utilizarse para especificar si la presencia del


atributo es esencial ("required"), opcional ("optional") o incluso si está prohibida
("prohibited"), aunque esta última opción no resulta especialmente útil.
Facetas
Las facetas forman parte de la definición de elementos y atributos de un esquema XML y nos
permiten especificar restricciones adicionales sobre los datos que pueden aparecer en un
documento XML válido:

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).

También podemos especificar el conjunto de valores permitidos para un tipo de dato:

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):

SKU (Stock Keeping Unit): Código para identificar productos


(p.ej. 976-FB)

<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:element name="comment" type="xsd:string"/>

<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:

<lista>20003 15037 95977 95945</lista>


y en el esquema XSD:

<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:element name="piezas" type="Piezas" />

</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>

Dado el esquema anterior, el siguiente documento XML sería válido:

<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

Herramientas de diseño para XML


Visual Studio incluye una herramienta (el diseñador XML) que nos permite hacer casi de todo
con documentos XML y esquemas XSD de una forma similar a como se trabaja con bases de
datos en Access.

Crear un esquema XML con el diseñador XML

Vamos a ilustrar cómo crear un esquema XML para un problema en el que se gestionan
pedidos.

1. Crear proyecto y agregar esquema XML.

 Crear un proyecto de Aplicación para Windows.

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

C# en el panel Plantillas y, a continuación, seleccione Aplicación para Windows.


Asigne al proyecto el nombre EjemploEsquema.

 Agregar un esquema XML al proyecto.

En el menú Proyecto, seleccione Agregar

nuevo elemento (o situados en el explorador de soluciones, pinchar con el botón derecho


sobre EjemploEsquema y seleccionar Agregar y Agregar nuevo elemento) y, a
continuación, haga doble clic en el icono Esquema XML en el cuadro de diálogo Agregar
nuevo elemento. Cambiar el nombre por EsquemaPedido. Aparecerá el Diseñador XML.

2. Definir los tipos de datos.

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.

 Agregar un objeto simpleType al proyecto.

 Si aún no está abierto, haga doble clic en el archivo EsquemaPedido.xsd para


abrir el Diseñador XML.
 Haga clic en la ficha Esquema XML del Cuadro de
 herramientas y arrastre un objeto simpleType hasta la superficie del
diseñador.
 Cambie el nombre del objeto simpleType haciendo clic en el primer cuadro de
texto del encabezado y reemplazando simpleType1 por CodigoProvincia.
 Establezca el tipo base del tipo CodigoProvincia haciendo clic en la lista
desplegable del encabezado y seleccionando string.
 Colóquese en la primera columna de la fila siguiente y seleccione facet en la
lista desplegable.
 Colóquese en la siguiente celda, seleccione lenght en la lista desplegable y
establezca el valor 2 en la siguiente columna.

De esta manera se exige que el valor escrito en el campo CodigoProvincia


tenga dos caracteres.

CodigoProvincia debe tener este aspecto en la vista de esquema:

 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>

 Este tipo simple CodigoProvincia se utilizará para definir el elemento


Provincia del tipo complejo que creará en la siguiente sección.

 Agregar objetos complexType al proyecto.


El tipo complejo TipoDireccion define un conjunto de elementos que aparecerán en
cualquier elemento con tipo TipoDireccion. Por ejemplo, un elemento del tipo FacturarA
incluirá información de nombres y dirección cuando se establece que un componente suyop
sea de tipo TipoDireccion. Mediante la creación del tipo complejo y utilizándolo en un
elemento, se genera una relación anidada.

 Haga clic en la ficha Esquema del Diseñador XML.


 Haga clic en la ficha Esquema XML del Cuadro de
 herramientas y arrastre un objeto complexType hasta la superficie del
diseñador.
 Cambie el nombre del tipo por TipoDireccion.
 Añada un elemento al tipo complejo haciendo clic en la primera celda de la
primera fila y seleccionando element en la lista desplegable. En la segunda
columna, cambie element1 por Nombre. En la tercera columna, acepte el
valor predeterminado string.
 Añada nuevos elementos al tipo complejo de manera que tenga un aspecto
similar al siguiente en la vista de esquema:

 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>

3. Crear una tabla relacional.

Cuando se arrastra el objeto element del Cuadro de herramientas a la superficie de


diseño, realmente se agrega un elemento que contiene un complexType sin nombre. Al incluir
el tipo complejo sin nombre se define el elemento para que sea una tabla relacional.

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.

 Agregar un elemento al proyecto.

 Haga clic en el Cuadro de herramientas y, en la ficha Esquema XML,


arrastre un objeto element hasta la superficie de diseño.
 Cambie element1 a Pedidos para asignar un nombre al elemento. Puede
dejar el tipo de datos como queda por defecto: (Pedidos).
 Agregue un elemento a Pedidos haciendo clic en la primera celda de la
primera fila y seleccionando element en la lista desplegable. Dele al
elemento el nombre EnviarA y establezca que su tipo es TipoDireccion.
El tipo Pedidos tendrá este aspecto en la vista de esquema:

 Añada otro elemento llamado FacturarA y establezca que su tipo es


TipoDireccion. El tipo Pedidos tendrá ahora este aspecto en la vista de
esquema:
y se habrá añadido el siguiente código XML:

<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>

 Crear otra tabla relacional anidada.

 Añadir un nuevo elemento a Pedidos. Llámele Items y establezca que su


tipo es Unnammed complexType.

 En el elemento Items, agregue un elemento, déle el nombre Item y


establezca que su tipo es Unnammed complexType. Cuando escriba el
elemento Item como anónimo, se agrega un elemento adicional a la
superficie de diseño, que es otra tabla relacional:
 Añada los siguientes elementos al objeto Item: Cantidad (tipo integer),
Precio (tipo decimal) e IDProducto (tipo integer).

Finamente, como el elemento Item puede repetirse indefinidamente para


un elemento Items seleccionar en el diseñador de esquemas el elemento
Item pinchando sobre su nombre y en la ventana de Propiedades
establecer que minOccurs sea 1 y que maxOccurs sea unbounded.

El código XML (completo) asociado a este esquema será, finamente:


<?xml version="1.0" encoding="utf-8" ?>
<xs:schema id="EsquemaPedido"
targetNamespace="http://tempuri.org/EsquemaPedido.xsd"
elementFormDefault="qualified"
xmlns="http://tempuri.org/EsquemaPedido.xsd"
xmlns:mstns="http://tempuri.org/EsquemaPedido.xsd"
xmlns:xs="http://www.w3.org/2001/XMLSchema">

<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 el menú Proyecto, seleccione Agregar

nuevo elemento (o situados en el explorador de soluciones, pinchar con el botón derecho


sobre EjemploEsquema y seleccionar Agregar y Agregar nuevo elemento) y, a
continuación, haga doble clic en el icono Archivo XML en el cuadro de diálogo Agregar
nuevo elemento. Cambiar el nombre por PedidosSeptiembre. Aparecerá una ventana de
edición conteniendo únicamente:

<?xml version="1.0" encoding="utf-8" ?>

listo para empezar a trabajar con él.

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:

que hace que el fichero XML tenga el siguiente contenido:

<?xml version="1.0" encoding="utf-8" ?>


<Pedidos xmlns="http://tempuri.org/EsquemaPedido.xsd">
</Pedidos>

Observar que automáticamente se ha escrito la etiqueta correspondiente al elemento raiz


Pedidos. Visual Studio .NET ayuda a la introducción de datos, ya que al escribir el ángulo de
apertura (<) nos indica qué elementos podemos utilizar:
Esta ayuda es contextual, por lo que si estamos completando el elemento EnviarA, la ayuda
ofrecida será:

Completar hasta escribir lo siguiente:

<?xml version="1.0" encoding="utf-8" ?>


<Pedidos xmlns="http://tempuri.org/EsquemaPedido.xsd">
<EnviarA>
<Calle>C/ Recogidas, 23</Calle>
<Ciudad>Granada</Ciudad>
<CodPostal>18003</CodPostal>
<Nombre>Joaquín Núñez Argüelles</Nombre>
<Provincia>Granada</Provincia>
</EnviarA>
<FacturarA>
<Calle>C/ Motril, 44</Calle>
<Ciudad>Albolote</Ciudad>
<CodPostal>18220</CodPostal>
<Nombre>VISTUDNET S.A.</Nombre>
<Provincia>Granada</Provincia>
</FacturarA>
<Items>
<Item>
<Cantidad>1</Cantidad>
<IDProducto>11111</IDProducto>
<Precio>200.45</Precio>
</Item>
<Item>
<Cantidad>3</Cantidad>
<IDProducto>11122</IDProducto>
<Precio>100.22</Precio>
</Item>
</Items>
</Pedidos>
Si cambiamos el modo de vista a Datos encontramos una interfaz cómoda que permite añadir,
modificar, borrar, etc:

Finalmente, si queremos validar los datos XML ( PedidosSeptiembre.xml) respecto al


esquema (EsquemaPedido.xsd) seleccionaremos XML en el menú principal y Validar
datos XML. Si todo es correcto en la barra de estado se indica que No se encontraron errores
de validación.
Un ejercicio: diseñar un esquema XML
Se trata de realizar un esquema XML y de crear un fichero de datos para guardar los datos de
nuestra biblioteca personal. La biblioteca consta de una serie (indeterminada en número) de
libros. Cada libro se identifica por: título, autor, número de páginas, precio y tipo de
encuadernación. Para hacer más fácil su clasificación se quieren registrar los géneros
(literarios) en los que puede encuadrarse el libro, teniendo en cuenta que un libro puede tener
asociado un número máximo de cinco géneros. Finalmente, queremos tener en cuenta si el
libro está disponible o no (se supone entonces que lo hemos prestado).

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.

1. Crear proyecto y agregar esquema XML.

Crear un proyecto de Visual C# llamado Bibioteca y agregar un esquema XML llamado


EsquemaBiblioteca.

2. Definir los tipos de datos.

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.

 Arrastre un objeto simpleType hasta la superficie del diseñador y cambie el


nombre del tipo por TipoPrecio, establezca el tipo base a float y
restrinja su valor mínimo a 0.0: colóquese en la primera columna de la fila
siguiente, seleccione facet en la lista desplegable, colóquese en la
siguiente celda, seleccione minInclusive en la lista desplegable y
establezca el valor 0.0 en la siguiente columna.

 Agregar un tipo complejo para representar a cada libro. El tipo complejo


TipoLibro define el conjunto de elementos que aparecerá en cualquier
elemento con tipo TipoLibro. Arrastre un objeto complexType hasta la
superficie del diseñador y cambie el nombre del tipo por TipoLibro, añada
los elementos al tipo complejo siguiendo las indicaciones de la figura:
3. Crear una tabla relacional.

La biblioteca consta de una serie o secuencia indefinida de elementos de tipo TipoLibro. Se


trata de añadir un elemento al esquema arrastrando un objeto de tipo element a la superficie
de dieño

Cambie element1 a Biblioteca para asignar un nombre al elemento. Puede dejar el tipo de
datos como queda por defecto: (Biblioteca).

En el elemento Biblioteca, agregue un elemento, déle el nombre Libro y establezca que su


tipo es TipoLibro:

Como el elemento Libro puede repetirse indefinidamente para un elemento Biblioteca


seleccionar en el diseñador de esquemas el elemento Libro pinchando sobre su nombre y en
la ventana de Propiedades establecer que minOccurs sea 1 y que maxOccurs sea
unbounded.

4. Gestión de los géneros.

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.

 Añada al tipo complejo TipoLibro un nuevo elemento lamado Generos de


tipo Unnamed conplexType. Observar que al añadir el elemento Generos
como anónimo, se agrega un elemento adicional a la superficie de diseño,
que es otra tabla relacional:

 Añada un elemento al objeto Generos llamdo Genero de tipo TipoGenero.


Finamente, como el elemento Genero puede repetirse entre 1 y 5 veces
seleccionar en el diseñador de esquemas el elemento Genero pinchando
sobre su nombre y en la ventana de Propiedades establecer que
minOccurs es 1 y que maxOccurs es 5:
El código XML (completo) asociado a este esquema ( EsquemaBiblioteca.xsd) será,
finamente:

<?xml version="1.0" encoding="utf-8" ?>


<xs:schema id="EsquemaBiblioteca"
targetNamespace="http://tempuri.org/EsquemaBiblioteca.xsd"
elementFormDefault="qualified"
xmlns="http://tempuri.org/EsquemaBiblioteca.xsd"
xmlns:mstns="http://tempuri.org/EsquemaBiblioteca.xsd"
xmlns:xs="http://www.w3.org/2001/XMLSchema">

<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>

5. Creación de un fichero XMl coherente con el esquema.

Añadir al proyecto un fichero XML (menú Proyecto, Agregar nuevo elemento, Archivo
XML) llamado LibrosDeCasa.xml.

Asociar a este fichero XML el esquema EsquemaBiblioteca.xsd: En la ventana de


Propiedades, seleccionar DOCUMENT, y en la propiedad targetSchema seleccionar el
esquema de la lista desplegable.

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.

<?xml version="1.0" encoding="utf-8" ?>


<Biblioteca
xmlns="http://tempuri.org/EsquemaBiblioteca.xsd">

<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

Hojas de estilo CSS


CSS es el acrónimo de Cascading Style Sheet (hoja de estilo en cascada).

CSS es un lenguaje de hojas de estilo que se utiliza normalmente para controlar la


presentación de documentos HTML, pero también se puede utilizar con documentos XML. Su
principal característica, en contraste con el lenguaje XSLT, es su sencillez.

Ventajas:

 Fácil de aprender y utilizar. Muchos desarrolladores ya lo conocen.


 No requiere la creación de una página HTML para visualizar código XML.
 Consume poca memoria y tiempo de proceso, pues no construye una representación
en árbol del documento.
 Muestra el documento según se va procesando.

Desventajas:

 Utiliza una sintaxis diferente a la del XML.


 Sólo sirve para visualizar documentos en un navegador.
 No es muy flexible:
o No permite realizar manipulaciones sobre el documento, tales como añadir
y borrar elementos, realizar ordenaciones, etc.
o Sólo permite acceder al contenido de los elementos, no a los atributos, no
permite instrucciones de proceso, etc.

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:

<?xml-stylesheet type="text/css" href="fichero.css" ?>


donde fichero.css es el nombre del fichero CSS que contiene las reglas de formato.

Un ejemplo sencillo

Un fichero de estilo sencillo MuySencillo.css asociado al fichero LibrosDeCasa.xml podría


ser:

/* 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;
}

Si se añade la línea oportuna en LibrosDeCasa.xml para usar el fichero de estilo


MuySencillo.css:

<?xml version="1.0" encoding="utf-8" ?>


<?xml-stylesheet type="text/css" href="MuySencillo.css" ?>

<Biblioteca xmlns="http://tempuri.org/EsquemaBiblioteca.xsd">

<Libro Disponible="false">
<Titulo>El Rabino</Titulo>
<Autor>Noah Gordon</Autor>
<NumPags>650</NumPags>
......

el resultado, visto en un navegador será:


Hojas de estilo CSS en Visual Studio .NET

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.

En el menú Proyecto, seleccione Agregar

nuevo elemento y, a continuación, haga clic en el icono Hoja de

estilos en el cuadro de diálogo Agregar nuevo elemento. Cambiar el nombre por


MiHojaCSS.css. Aparecerá el Diseñador de hojas CSS:

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:

Para establecer el formato de los elementos Titulo tenemos tres posibilidades:

 A) Pinchar con el botón derecho sobre Titulo en el explorador de elementos y


seleccionar Generar estilo.
 B) En la ventana Propiedades, en la propiedad Style pinchar en el botón de
elipsis.
 C) Escribir manualmente las transformaciones a aplicar. Requiere mucha experiencia.

Se abrirá la ventana de diálogo Generador de

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;
}

y comprobar que el resultado es:


Transformaciones XSLT
Introducción

XSLT es el acrónimo de EXtensible Stylesheet Languaje Transformation.

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).

Para aclarar terminología, XSL es un estándar que consiste en:

 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.

La principal característica del lenguaje XSLT es su potencia. No es sólo un lenguaje para


visualizar documentos, sino en general para transformarlos y manipularlos. Esta
manipulación la gestiona un programa especial que se llama procesador XSLT.

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:

 Su utilización es más compleja.


 Consume cierta memoria y capacidad de proceso, pues se construye un árbol con el
contenido del documento.

Formas de uso

 Visualizar directamente en un navegador el documento XML que tiene asociada una


hoja XSLT. El navegador debe tener incorporado un procesador XSLT.
 Ejecutar el procesador XSLT independientemente del navegador. Se le pasan las
entradas necesarias (fichero origen y hoja XSLT a utilizar) y genera la salida en un
fichero, con el que podemos hacer lo que queramos.
 Realizar las transformaciones dentro de un programa en el servidor y enviar a los
clientes sólo el resultado de la transformación.

Crear documentos XSLT en Visual Studio .NET

En el menú Proyecto, seleccione Agregar

nuevo elemento y, a continuación, haga clic en el icono Archivo

XSLT en el cuadro de diálogo Agregar nuevo elemento. Cambiar el nombre por


XSLTSencillo.xslt.

Muy importante: Siempre se emplea el espacio de nombres


xmlns="http://www.w3.org/1999/XSL/Transform". Modificar el código XML que escribe
Visual Studio .NET en el fichero XSLTSencillo.xslt para que el prefijo del espacio de
nombres sea xsl:

<!-- Fichero: XSLTSencillo.XSLT -->

<?xml version="1.0" encoding="UTF-8" ?>


<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

</xsl:stylesheet>

Para usar un fichero XSLT desde un documento XML hay que añadir la siguiente línea en el
prólogo:

<?xml-stylesheet type="text/xsl" href="fichero.xslt" ?>


donde fichero.xslt es el nombre del fichero XSLT.

Por ejemplo, modificaremos el fichero LibrosDeCasa.xml para que quede como sigue:

<!-- Fichero: LibrosDeCasa.xml -->

<?xml version="1.0" encoding="utf-8" ?>

<?xml-stylesheet type="text/xsl" href="XSLTSencillo.xslt"


?>

<Biblioteca>

<Libro Disponible="false">
<Titulo>El Rabino</Titulo>
<Autor>Noah Gordon</Autor>
<NumPags>650</NumPags>
......

Solo falta escribir las transformaciones a aplicar en el fichero XSLT ...


Escribir transformaciones XSLT

 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.

<xsl:template match="PATRON"> <!-- Apertura -->


<!-- Esta es la parte central -->
</xsl:template> <!-- 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.

RESUMEN: ¿Cómo se realiza la transformación?

 El documento origen se pasa al procesador XSLT.


o (El procesador carga una hoja de estilo XSLT)
 El procesador entonces:
o Carga los patrones especificadas en la hoja de estilo...
o Recorre el documento XML origen, nodo por nodo...
o Si un nodo se ajusta a un patrón:
 Aplica las acciones especificadas, y
 Proporciona el resultado en un nuevo documento en formato XML
o HTML.

Otros elementos de XSL

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 />

 Puede restringirse con el atributo select para especificar un subconjunto de nodos.

<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:if sirve para evaluar condiciones sobre valores de atributos o elementos.

<xsl:if test="EXPRESIÓN LÓGICA">


.....
<xsl:if />

 xsl:choose sirve para evaluar condiciones múltiples (tipo switch).

<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 ordena un conjunto de nodos de acuerdo a algún elemento. Por ejemplo,


para ordenar de forma creciente según el número de páginas:

<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:

 Es declarativo (vs. procedimental).


 Es contextual ya que los resultados dependen del nodo "actual".
 Admite expresiones comunes: operadores de comparación, lógicos y matemáticos ( =,
<, and, or, *, +, etc.)

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.

/LIBROS[@TEMA="XML"] Selecciona todos los elementos LIBROS de la raiz (los


seleccionados antes) y de éstos selecciona aquellos que tengan el valor XML en el atributo
TEMA.

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.

<!-- Fichero: XSLTSencillo.xslt -->


<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/Biblioteca">
<h2>LISTA DE LIBROS</h2>
<xsl:apply-templates />
<hr/>
</xsl:template>
<xsl:template match="Libro">
<p><xsl:value-of select="Titulo" /></p>
</xsl:template>
</xsl:stylesheet>

Recordemos que en el fichero XML (LibrosDeCasa.xml) debemos indicar el fichero de


transformación:

<!-- Fichero: LibrosDeCasa.xml -->


<?xml version="1.0" encoding="utf-8" ?>
<?xml-stylesheet type="text/xsl"
href="XSLTSencillo.xslt"?>

<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:

<!-- Fichero: XSLTSencilloHTML.xslt -->


<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<HTML>
<HEAD><TITLE>Lista de libros</TITLE></HEAD>
<BODY BGCOLOR="#FFFF00">
<xsl:apply-templates />
</BODY>
</HTML>
</xsl:template>
<xsl:template match="Biblioteca">
<H2>Mis libros de casa:</H2>
<xsl:apply-templates />
<hr/>
</xsl:template>
<xsl:template match="Libro">
<P>
<xsl:apply-templates select="Titulo"/>
(<xsl:apply-templates select="Autor"/>)
</P>
</xsl:template>
<xsl:template match="Titulo">
<FONT COLOR="#0000FF">
<xsl:value-of select="." />
</FONT>
</xsl:template>

<xsl:template match="Autor">
<FONT COLOR="#FF0000">
<xsl:value-of select="." />
</FONT>
</xsl:template>
</xsl:stylesheet>
El fichero XML quedará ahora:

<!-- Fichero: LibrosDeCasa.xml -->

<?xml version="1.0" encoding="utf-8" ?>

<?xml-stylesheet type="text/xsl"
href="XSLTSencilloHTML.xslt"?>

<Biblioteca>
......

Ejemplo 3

En este ejemplo practicamos con la construcción iterativa xsl:for-each para mostrar un


informe detallado de cada libro.

<!-- Fichero: InformeGeneros.xslt -->

<?xml version="1.0" encoding="UTF-8" ?>

<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.

<!-- Fichero: ListaVoluminosos.xslt -->

<?xml version="1.0" encoding="UTF-8" ?>

<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>

<P>Los libros de la biblioteca que tienen más de


500 páginas son: </P>

<OL>
<xsl:apply-templates select="Libro" />
</OL>
</BODY>
</xsl:template>

<!-- *********************************************** -->

<xsl:template match="Libro">

<xsl:if test="NumPags &gt; 500">


<LI><B><xsl:value-of select="Titulo"/></B>
(<I><xsl:value-of select="Autor"/></I>):
<xsl:value-of select="NumPags"/> págs.
</LI>
</xsl:if>

</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:

 "Declarar" una variable y darle un valor:

<!-- Fichero: ListaVoluminosos.xslt -->

<?xml version="1.0" encoding="UTF-8" ?>

<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:param name="NumPagsVol" select="500" />


......

 Cambiar las referencias explícitas al valor por su nombre:


o Si va a mostrarse textualmente:

......
<P>Los libros de la biblioteca que tienen más de
<xsl:value-of select="$NumPagsVol"/>
páginas son: </P>
......

o Si se va a usar en una expresión:

......
<xsl:if test="NumPags &gt; $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.

<!-- Fichero: TablaResumen.xslt -->

<?xml version="1.0" encoding="UTF-8" ?>

<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:apply-templates select="Biblioteca" />


</HTML>

</xsl:template>

<!-- *********************************************** -->

<xsl:template match="Biblioteca">

<BODY BGCOLOR="#FAFAFA">

<H2 ALIGN="CENTER">Mis libros ordenados según


el número de paginas</H2>

<TABLE ALIGN="CENTER" BORDER="2">


<encabezado-tabla>
<TR BGCOLOR="#FFFF00"><TH>Titulo</TH><TH>Autor</TH>
<TH>Paginas</TH><TH>Precio</TH><TH>Disponible</TH>
<TH>Generos</TH></TR>
</encabezado-tabla>

<xsl:for-each select="Libro">

<xsl:sort select="NumPags" order="descending" />

<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

Modelo de objetos de documento (DOM)


Introducción

DOM (Document Object Model o modelo de objetos de documento) es un estándard de la


W3C que especifica la forma de acceder y manipular los datos de un documento XML mediante
un programa.

DOM especifica una representación en la memoria de un documento XML de manera jerárquica


mediante un árbol de nodos (elementos, comentarios, entidades, atributos, etc).

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.

Tipo de nodo Objeto (clase


Descripción
DOM .NET)
Contenedor de todos los nodos
del árbol. También se conoce
Document XmlDocument como la raíz del documento,
que no siempre coincide con el
elemento raíz.
Representa un nodo de
Element XmlElement
elemento.
Attr XmlAttribute Atributo de un elemento.
Comment XmlComment Nodo de comentario.
Texto que pertenece a un
Text XmlText
elemento o atributo.
CDATASection XmlCDataSection Representa CDATA.

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:

 Un nodo tiene un único nodo primario, que se encuentra directamente encima


de él. El único nodos que no tiene un nodo primario es la raíz.
 La mayor parte de los nodos pueden tener varios nodos secundarios, que son
los que están situados inmediatamente debajo de ellos. Los tipos de nodo
que pueden tener nodos secundarios son:
o Document
o DocumentFragment
o EntityReference
o Element
o Attribute
 Los nodos que se encuentran en el mismo nivel son nodos relacionados.

Acceso a los atributos

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:

 Para tener un acceso rápido, sólo hacia delante y sin almacenamiento


en caché a secuencias de XML, utilice XmlReader y XmlWriter.
 Si necesita acceso aleatorio con un modelo de cursor y XPath, utilice la
clase XPathNavigator.
Espacio de nombres System.Xml
El espacio de nombres System.Xml proporciona compatibilidad basada en normas para
procesar XML. Se admiten las siguientes normas:

 XML 1.0: http://www.w3.org/TR/1998/REC-xml-19980210, incluida


compatibilidad con DTD.
 Espacios de nombres XML: http://www.w3.org/TR/REC-xml-names/, tanto
a nivel de secuencias como DOM.
 Esquemas XSD: http://www.w3.org/2001/XMLSchema
 Expresiones XPath: http://www.w3.org/TR/xpath
 Transformaciones XSLT: http://www.w3.org/TR/xslt
 Core DOM Level 1: http://www.w3.org/TR/REC-DOM-Level-1/
 Core DOM Level 2: http://www.w3.org/TR/DOM-Level-2/

Más información en: System.Xml (Espacio de nombres)

La plataforma .NET proporciona soporte para XML en el espacio de nombres System.Xml y los
subespacios:

 System.Xml.Xsl.

El espacio de nombres System.Xml.Xsl proporciona compatibilidad con


transformaciones XSLT (Extensible Stylesheet Transformation). Admite la
Recomendación XSL Transformations (XSLT) Version 1.0 del Consorcio W3C
(www.w3.org/TR/xslt).

Más información en: System.Xml.Xsl (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).

Más información en: System.Xml.XPath (Espacio de nombres)

 System.Xml.Schema.

El espacio de nombres System.Xml.Schema contiene las clases XML que


proporcionan compatibilidad basada en normas para los esquemas de XSD
ó lenguaje de definición de esquemas XML (www.w3.org/XML/Schema).

Más información en: System.Xml.Schema (Espacio de nombres)

 System.Xml.Serialization.

El espacio de nombres System.Xml.Serialization contiene clases que


se utilizan para serializar objetos en secuencias o documentos con formato
XML.

Más información en: System.Xml.Serialization (Espacio de nombres)

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.

La clase base XmlNode representa un nodo individual de la jerarquía XML y proporciona


propiedades y métodos para:

 Recorrer jerárquicamente un documento XML.


 Consulta y selección de nodos.
 Modificar nodos.
 Borrar nodos.

Más información en: XmlNode (Clase)

Breve descripción de las clases derivadas de XmlNode:

 XmlDocument. Representa un documento XML según el modelo DOM y


posibilita la exploración y edición del documento.
 XmlLinkedNode. Recupera el nodo que antecede o sigue al nodo actual. Es
una clase abstracta de la que deriva la clase XmlElement.
 XmlElement. Representa un elemento del árbol DOM. Los elementos pueden
tener atributos asociados. Dispone de muchos métodos para tener acceso a
los atributos (GetAttribute, SetAttribute, RemoveAttribute,
GetAttributeNode, etc.). Además, puede utilizar la propiedad
Attributes que devuelve una XmlAttributeCollection, que le
permite tener acceso a los atributos por nombre o por índice de la
colección.
 XmlAttribute. Representa un atributo de un XmlNode. Los valores válidos y
predeterminados del atributo se definen en una DTD o un esquema. Utilice
la propiedad OwnerElement para obtener el XmlElement al que pertenece
el atributo.
 XmlDataDocument. Extiende XmlDocument y permite que los datos
estructurados se almacenen, recuperen y manipulen mediante un DataSet
relacional. Esta clase permite que los componentes combinen vistas XML y
relacionales de los datos subyacentes.
La clase XmlDocument
La clase XmlDocument implementa la clase XmlNode y representa un documento XML
completo según el modelo DOM y posibilita la exploración y edición del documento.

Cuando se instancia la clase XmlDocument puede leerse un documento XML utilizando el


método Load.

Dado que XmlDocument implementa la interfaz IXPathNavigable, también se puede utilizar


como documento de origen de la clase XslTransform.

Más información en: XmlDocument (Clase)

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.

Crea un objeto XmlComment que contiene los datos


CreateComment
especificados.
CreateElement Crea un objeto XmlElement.
CreateNode Crea un objeto XmlNode.
CreateTextNode Crea un objeto XmlText con el texto especificado.

Devuelve un objeto XmlNodeList que contiene una lista de


GetElementsByTagName todos los elementos descendientes que coinciden con el
nombre especificado.
Inserta el nodo especificado inmediatamente detrás del
InsertAfter
nodo de referencia igualmente especificado.
Inserta el nodo especificado inmediatamente antes del nodo
InsertBefore
de referencia igualmente especificado.
Carga los datos XML especificados.

Nota: El método Load conserva siempre bastante espacio


Load en blanco. La propiedad PreserveWhitespace determina
si se conserva o no espacio en blanco. El valor
predeterminado es false, es decir, el espacio en blanco no
se conserva.
LoadXML Carga el documento XML desde la cadena especificada.
Agrega el nodo especificado al principio de la lista de nodos
PrependChild
secundarios de este nodo.
Quita todos los atributos y nodos secundarios del nodo
RemoveAll
actual.
RemoveChild Quita el nodo secundario especificado.
ReplaceChild Reemplaza un nodo secundario.
Save Guarda el documento XML en la ubicación especificada.
Selecciona una lista de nodos que coinciden con la
SelectNodes
expresión XPath.
Selecciona el primer XmlNode que coincide con la expresión
SelectSingleNode
XPath.

El siguiente código copia en la consola el contenido del fichero XML LibrosDeCasa.xml:

XmlDocument doc = new XmlDocument();


doc.Load ("../../LibrosDeCasa.xml");
doc.Save (Console.Out);
Ejemplo: cáculo de estadísticas

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
{

static void Main(string[] args)


{

// Crear el documento en memoria y cargarlo

XmlDocument doc = new XmlDocument();


doc.Load ("../../LibrosDeCasa.xml");

// Selección de libros usando expresion XPATH

XmlNodeList ListaLibros =
doc.SelectNodes ("/Biblioteca/Libro");

// Calcular el numero de libros

int nl = ListaLibros.Count;
Console.WriteLine ("Numero total de libros = {0}", nl);

double SumPrecio = 0.0;


double SumPags = 0;

// Calcular precio medio (Observar la seleccion


// que se realiza dentro de la seleccion)

foreach (XmlNode libro in ListaLibros)


{
string sP = libro.SelectSingleNode("Precio").InnerText;
double PrecioLibro = XmlConvert.ToDouble(sP);
SumPrecio += PrecioLibro;
}
Console.WriteLine (" Precio medio = {0} Euros",
SumPrecio / nl);

// Calcular tamaño medio (Observar que se hace


// una única selección -preferible-)

XmlNodeList ListaPrecios =
doc.SelectNodes ("/Biblioteca/Libro/NumPags");

for (int i=0; i < ListaPrecios.Count; i++)


{
int PagsLibro =
XmlConvert.ToInt16(ListaPrecios[i].InnerXml);
SumPags += PagsLibro;
}
Console.WriteLine (" Tamaño medio = {0} págs.",
SumPags / nl);
// Contar y mostrar los titulos de los libros
// disponibles. Observar la seleccion con atributo

string filtro = "/Biblioteca/Libro[@Disponible='true']";


XmlNodeList ListaDisponibles = doc.SelectNodes (filtro);

Console.WriteLine ("Libros disponibles = {0}",


ListaDisponibles.Count);

foreach (XmlNode LibroDisponible in ListaDisponibles)


{
string tit =
LibroDisponible.SelectSingleNode("Titulo").InnerText;
string aut =
LibroDisponible.SelectSingleNode("Autor").InnerText;
Console.Write (" " + tit);
Console.WriteLine (" (" + aut + ")");
}

Console.ReadLine();
}
}

Acceso a los atributos

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.

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 =

atrCol["ISBN"] en el siguiente ejemplo) o por número de índice (sustituir la anterior por


XmlAttribute atr = atrCol[1]).

using System;
using System.IO;
using System.Xml;

class BibliotecaApp
{

static void Main(string[] args)


{
// Crear el documento en memoria

XmlDocument doc = new XmlDocument();


doc.LoadXml("<Libro Genero='Novela' " +
"ISBN='1-861001-57-5' " +
"Comentario='mal estado'>" +
"<Titulo>El oro del Rey</Titulo>" +
"<Precio>14.95</Precio>" +
"</Libro>");

// Colocarse en la raiz del documento.

XmlElement Raiz = doc.DocumentElement;

// Recuperar los atributos en una coleccion.

XmlAttributeCollection atrCol = Raiz.Attributes;

// De manera resumida:
// XmlAttributeCollection atrCol =
// doc.DocumentElement.Attributes;

// Mostrar los atributos de la coleccion iterando


// sobre los elementos de la coleccion.

Console.WriteLine("Atributos en la coleccion: ");

for (int i=0; i < atrCol.Count; i++)


{
Console.Write(" {0} = ", atrCol[i].Name);
Console.Write(" {0}", atrCol[i].Value);
Console.WriteLine();
}

// Recuperar un unico atributo (el llamado ISBN)

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();
}
}

Recuperar un nodo de atributo individual.

Para recuperar un solo nodo


de atributo de un elemento, se utiliza el método
XmlElement.GetAttributeNode. Este método devuelve un objeto de tipo XmlAttribute.
Una vez que se tiene el objeto XmlAttribute, todos los métodos y propiedades de la clase de
miembros XmlAttribute están disponibles en ese objeto, como OwnerElement.

..........
// 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.

La clase XmlReader contiene métodos que permiten:

 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.

La clase XmlReader tiene propiedades que devuelven información, como:

 Tipo y nombre del nodo actual.


 Contenido del nodo actual.

La clase XmlTextReader es una implementación de la clase base XmlReader que permite


tomar texto de entrada de flujos y ofrece métodos y propiedades para tratr elementos y
atributos además de posibilitar la validación del texto XML.

Otras clases derivadas de XmlReader son:

 XmlNodeReader. Representa un lector que proporciona acceso rápido a datos


XML, sin almacenamiento en caché y con desplazamiento sólo hacia delante
en un XmlNode. XmlNodeReader puede leer un subárbol DOM XML. Esta
clase no admite la validación de DTD ni de esquemas. Para realizar la
validación de datos, utilice XmlValidatingReader.
 XmlValidatingReader. Representa un lector que proporciona validación de
esquemas para DTD, esquemas reducidos de datos XML (esquemas XDR) y
esquemas del lenguaje de definición de esquemas XML (esquemas XSD).

Más información en: XmlReader (Clase)

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:

 Especificar si se debe permitir el uso de espacios de nombres.


 Escribir código XML con un formato correcto.
 Administrar la salida, lo que incluye a los métodos para determinar su
progreso, con la propiedad WriteState.
 Escribir varios documentos en una secuencia de salida.
 Vaciar o cerrar la secuencia de salida.
 Escribir nombres válidos, nombres completos y símbolos (tokens) de
nombres.

La clase XmlTextWriter es una implementación de la clase base XmlWriter que incorpora


propiedades y métodos para escribir XML sintácticamente correcto.

Más información en: XmlWriter (Clase)


El espacio de nombres System.Xml.Xsl
Copmo indicamos anteriormente System.Xml.Xsl proporciona clases y soporte para realizar
transformaciones XSLT. El objetivo de XSLT (Extensible Stylesheet Language Transformation)
es transformar el contenido de un documento XML de origen en otro documento con un
formato o estructura diferentes.

Más información en: System.Xml.Xsl (Espacio de nombres)

La clase XslTransform (se encuentra en este espacio de nombres) es el procesador de XSLT


que implementa la funcionalidad de esta especificación. Otra clase útil de System.Xml.Xsl
es:

 XsltException. Excepción que se produce cuando ocurre algún error


mientras se procesa una transformación XSL (Extensible Stylesheet
Language).

En la ilustración siguiente se muestra la arquitectura de transformación de .NET Framework.

La recomendación de XSLT utiliza XPath para seleccionar componentes de un documento XML.


XPath es un lenguaje de consulta utilizado para explorar los nodos de un árbol de documentos.
Tal y como se muestra en el diagrama, la implementación de XPath en .NET Framework se
utiliza para seleccionar partes de XML almacenadas en varias clases, como XmlDocument,
XmlDataDocument y XPathDocument. XPathDocument es un almacén de datos optimizado
de XSLT y, si se utiliza con XslTransform, proporciona transformaciones XSLT de alto
rendimiento.

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

Empleando el fichero de transformación TransfSimple.xslt:

<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>

transformaremos el fichero LibrosDeCasa.xml de manera que el resultado de la


transformación se muestre en la consola.

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");

// Aplicar la transformación y presentar el


// resultado en la consola.

xslt.Transform(doc, null, Console.Out, null);

Console.ReadLine();
}
}

Para que el resultado de la transformación se escriba en un fichero se emplea una versión


sobrecargada del método Transform:
.........
// Aplicar la transformación y guardar el
// resultado en un fichero.

xslt.Transform("../../LibrosDeCasa.xml",
"../../Resumen.xml", null);
.........

aunque el resultado, al igual que en la consola, no queda formateado correctamente:

Podemos hacer que el resultado quede con el formato adecuado si empleamos un


XmlTextWriter y empleamos sobre él la propiedad Formatting. Por ejemplo:

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");

// Aplicar la transformación y presentar el


// resultado en la consola.

XmlTextWriter writer =
new XmlTextWriter (Console.Out);
writer.Formatting = Formatting.Indented;
writer.Indentation = 4;
writer.WriteStartDocument(true);

xslt.Transform(doc, null, writer, null);

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).

Las clases más importantes proporcionadas en el espacio de nombres System.Xml.XPath


son:

 XPathDocument. Proporciona una caché rápida y de sólo lectura para el


procesamiento de documentos XML mediante XSLT. Esta clase está
optimizada para el procesamiento XSLT y el modelo de datos XPath.
 XPathNavigator. Lee datos de cualquier almacén de datos mediante un
modelo de cursor.
 XPathNodeIterator. Proporciona un iterador para un conjunto de nodos
seleccionados.

Más información en: System.Xml.XPath (Espacio de nombres)

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();

// Selecciona un conjunto de nodos utilizando la


// expresión XPath especificada.
XPathNodeIterator it =
nav.Select("/Biblioteca/Libro/Precio");

int nl = it.Count;
Console.WriteLine ("Numero total de libros= {0}",nl);

// Iterador con MoveNext()


double SumPrecio1 = 0.0;
while (it.MoveNext())
SumPrecio1 +=
XmlConvert.ToDouble(it.Current.Value);

Console.WriteLine (" Precio medio (1) = {0} Euros",


SumPrecio1 / 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");

// Crea un objeto XPathNavigator (nav) para


// desplazarse por doc
XPathNavigator nav = doc.CreateNavigator();

// Selecciona un conjunto de nodos utilizando la


// expresión XPath especificada.
XPathNodeIterator it =
nav.Select("/Biblioteca/Libro/Precio");

int nl = it.Count;
Console.WriteLine ("Numero total de libros= {0}",nl);

// Compila una cadena que representa a una expresión


// XPath y devuelve una expresion XPath
XPathExpression expr =
nav.Compile("sum(/Biblioteca/Libro/Precio)");

// Evalua la expresion
double SumPrecio2 = (double) nav.Evaluate(expr);

Console.WriteLine (" Precio medio (2) = {0} Euros",


SumPrecio2 / nl);

Console.ReadLine();
}
}

El siguiente ejemplo recorre el fichero de libros mostrando la lista de títulos y número de


páginas. Despues muestra el número medio de páginas.

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");

// Crea un objeto XPathNavigator (nav) para


// desplazarse por doc
XPathNavigator nav = doc.CreateNavigator();

// Accede al primer hijo de "doc": <Biblioteca>


nav.MoveToFirstChild();

int nl= 1;
int sum = 0;

// Accede al primer <Libro>


nav.MoveToFirstChild();

// Escibe los datos del primer libro

Console.Write ("Libro {0}: ", nl);

// Accede y pinta el primer campo (<Titulo>)


nav.MoveToFirstChild();
Console.Write (nav.Value);

// Saltar campo <Autor> y colocarse en <NumPags>


for (int i=0; i<2; i++)
nav.MoveToNext();

Console.WriteLine (" ({0} págs.)", nav.Value);


sum += XmlConvert.ToInt16(nav.Value);

// Volver al nodo <Libro>


nav.MoveToParent();

// Repetir para los restantes nodos <Libro>


while (nav.MoveToNext())
{
Console.Write ("Libro {0}: ", ++nl);

// Accede y pinta el primer campo <Titulo>


nav.MoveToFirstChild();
Console.Write (nav.Value);

// Saltar campo <Autor> y colocarse en <NumPags>


for (int i=0; i<2; i++)
nav.MoveToNext();

Console.WriteLine (" ({0} págs.)", nav.Value);


sum += XmlConvert.ToInt16(nav.Value);

// Volver al nodo <Libro>


nav.MoveToParent();
}

Console.WriteLine ("\nTotal de páginas = {0}", sum);


Console.WriteLine ("Tamaño medio = {0}",
(float)sum / nl);

Console.ReadLine();
}
}
"Serialización" de documentos XML
Introducción

El objetivo primario de la serialización en .NET es permitir la conversión de documentos XML y


flujos (streams) en objetos CLR y viceversa. Esta conversión se realizará en tiempo de
ejecución. Esta conversión supone que la manipulación de los objetos será más sencilla que
hacerlo con otras técnicas comunes (programas que hacen uso de DOM, por ejemplo).

En lugar de serialización debía llamarse transformación:

 No serializa cualquier objeto CLR en XML.


 Está diseñada para transformar clases a esquemas e instancias de clases a
instancias de esquemas.
 El objetivo es representar cualquier XML, no cualquier objeto.

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.

Los métodos fundamentales de la clase XmlSerializer son: Serialize y Deserialize.


Otro método importante es CanDeserialize.

Más información en: XmlSerializer (Clase)

using System;
using System.IO;
using System.Xml.Serialization;

namespace XSDSample
{
public class Test
{
public String cad;
}

class SerialApp {

static void Main(string[] args)


{
Test t = new Test();
t.cad = "Cadena de prueba";

XmlSerializer ser = new XmlSerializer (typeof(Test));


FileStream stream = new FileStream ("test.xml",
FileMode.OpenOrCreate);

ser.Serialize (stream, t);


}
}
}
Algunos puntos a tener en cuenta son:

 Solo pueden serializarse las clases que tienen un constructor público y


predeterminado.

// 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;
}

 Solo pueden serializarse campos y propiedades public.

Por ejemplo, la siguiente clase tiene todos sus miembros private:

// Clase serializable sin miembros serializables


public class Cliente
{
private String Nombre;
private String Direccion;
private String Telefono;
}

y al serializarla se obtiene el siguiente resultado:

<?xml version="1.0"?>
<Cliente
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
/>

Sin embargo, cuando los campos de la clase son public

// Clase serializable con miembros serializables


public class Cliente
{
public String Nombre;
public String Direccion;
public String Telefono;
}
.....
Cliente c = new Cliente();
c.Nombre = "Pepe";
c.Direccion = "Casa de Pepe";
c.Telefono = "123456789";
XmlSerializer ser = new XmlSerializer (typeof(Cliente));
FileStream stream = new FileStream ("cliente.xml",
FileMode.OpenOrCreate);

ser.Serialize (stream, c);


.....

el resultado es muy diferente:

<?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>

 Los campos de solo lectura (readonly) y las propiedades no son


serializables.
 Los métodos y cualquier información de tipos no son serializables.

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;

public override string ToString() {


string s1 = "Un cliente:"+"\n";
string s2 = " Nombre: " + this.Nombre + "\n";
string s3 = " Dirección: " + this.Direccion + "\n";
string s4 = " Teléfono: " + this.Telefono+ "\n";
return (s1+s2+s3+s4);
}
}

class SerDeserSampleApp {
static void Main(string[] args)
{
Cliente c = new Cliente();
c.Nombre = "Juan";
c.Direccion = "Casa de Juan S/N";
c.Telefono = "333444555";

Console.WriteLine ("Antes de guardar");


Console.WriteLine (c);

// Serializar
XmlSerializer ser = new
XmlSerializer (typeof(Cliente));
FileStream OStream = new FileStream ("Juan.xml",
FileMode.OpenOrCreate);

ser.Serialize (OStream, c);


OStream.Close();

// Deserializar
Cliente c2;

XmlSerializer ser2 = new


XmlSerializer(typeof(Cliente));
FileStream IStream = new
FileStream("Juan.xml", FileMode.Open);
XmlReader reader = new XmlTextReader(IStream);

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>

y en la consola se muestra este resultado:

Personalizar la serialización

Pueden emplearse atributos en las clases y sus miembros que modifican el XML resultante de
la serialización.

 Los atributos XmlRoot y XmlElement se emplean para especificar el nombre


de un elemento XML. Si no se especifican, las etiquetas del documento XML
son los nombres de los campos de la clase.

Observar el siguiente ejemplo:

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;

public override string ToString() {


string s1 = "Un cliente:"+"\n";
string s2 = " Nombre: " + this.Nombre + "\n";
string s3 = " Dirección: " + this.Direccion + "\n";
string s4 = " Teléfono: " + this.Telefono+ "\n";
return (s1+s2+s3+s4);
}
}
[XmlRoot("cliente")]
public class BuenCliente {
public string Telefono;
public string Direccion;
public string Nombre;

public override string ToString() {


string s1 = "Un buen cliente:"+"\n";
string s2 = " Nombre: " + this.Nombre + "\n";
string s3 = " Dirección: " + this.Direccion + "\n";
string s4 = " Teléfono: " + this.Telefono+ "\n";
return (s1+s2+s3+s4);
}
}
class SerDeserSampleApp {
static void Main(string[] args)
{
Cliente c = new Cliente();
c.Nombre = "Pepe";
c.Direccion = "Casa de Pepe";
c.Telefono = "123456789";

Console.WriteLine (c);

// Serializar
XmlSerializer ser = new
XmlSerializer (typeof(Cliente));
FileStream OStream = new FileStream ("cli.xml",
FileMode.OpenOrCreate);

ser.Serialize (OStream, c);


OStream.Close();

// Deserializar
BuenCliente bc;

XmlSerializer ser2 = new


XmlSerializer(typeof(BuenCliente));
FileStream IStream = new
FileStream("cli.xml", FileMode.Open);
XmlReader reader = new XmlTextReader(IStream);

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>

y en la consola se muestra este resultado:

Hay que tener ciertas precauciones en el proceso de deserialización, ya que


éste no es directo:

o La clase destino, si es diferente a la origen, debe conocer el nombre


del elemento raíz del XML. Por ejemplo, en la siguiente situación no
puede deserializarse hacia la clase BuenCliente:

public class Cliente {


public string Nombre;
public string Direccion;
public string Telefono;
......
}

public class BuenCliente {


public string Telefono;
public string Direccion;
public string Nombre;
......
}

o Para poder hacerlo podríamos añadir un atributo a la clase destino:

[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;
......
}

o El mismo criterio se sigue para los demás elementos XML que se


copian a la clase destino: o coinciden en nombre con los campos de la
clase o se especifica un atributo XmlElement en la declaración de la
clase:

[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;
......
}

o En este ejemplo sólo se deserializan los elementos Telefono y


Nombre (del documento XML) en los campos Tfno y Nbre (de la clase
BuenCliente).
 Puede modificarse el espacio de nombres predeterminado con el parámetro
Namespace

[XmlRoot("MiCliente", Namespace="http://rocco.ugr.es")]
public class Cliente
{
public string Nombre;
public string Direccion;
public string Telefono;
}

 Al serializar esta clase obtenemos (fichero cli3.xml):


<?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">
<Nombre>Pepe</Nombre>
<Direccion>Casa de Pepe</Direccion>
<Telefono>123456789</Telefono>
</MiCliente>

 Puede modificarse el espacio de nombres para algunos elementos:

[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;
}

public class TDireccion {


public string Calle;
public int Numero;
public string Ciudad;
public string CP;
}
......
Cliente c = new Cliente();
c.Direccion = new TDireccion();
c.Nombre = "Pepe";
c.Direccion.Calle = "Calle Fina";
c.Direccion.Numero = 4;
c.Direccion.Ciudad = "Metrópolis";
c.Direccion.CP = "12345";
c.Telefono = "123456789";
......

 Al serializar esta clase obtenemos (fichero cli4.xml):

<?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;
}

public class TDireccion {


public string Calle;
public int Numero;
public string Ciudad;
[XmlAttribute]
public string CP;
}

 Al serializar esta clase obtenemos (fichero cli5.xml):

<?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>

 Al serializar vectores cada entrada del vector se transforma en un elemento


de un elemento complejo:

public string[] Telefono;

<Telefono>
<string>123456789</string>
<string>987654321</string>
......
</Telefono>

 o se puede cambiar el nombre al conjunto y a cada elemento:


[XmlArray("TelefonosContacto")]
[XmlArrayItem("NumTelefono")]
public string[] Telefono;

<TelefonosContacto>
<NumTelefono>123456789</NumTelefono>
<NumTelefono>987654321</NumTelefono>
......
</TelefonosContacto>

Un ejemplo complejo de serialización

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:

Las estructuras de datos básicas para la aplicación son:

 La clase CLibro, que caracteriza a cada objeto libro de la colección. Observar


que pensando en la serialización de los objetos de tipo CLibro para que se
conviertan en los elementos XML ya conocidos hemos indicado que el
campo Disponible se codifique como un atributo y que el vector de
cadenas llamado Generos se codifique como el elemento sinónimo
mientras que cada elemento del vector se codifique como un elemento de
nombre Genero:
public class CLibro {

public string Titulo;


public string Autor;
public int NumPags;
public double Precio;

[XmlAttribute]
public bool Disponible;

[XmlArray("Generos")]
[XmlArrayItem("Genero")]
public string[] Generos;
}

 La clase Lista, que caracteriza a una colección de libros, implementada como


un vector de elementos de tipo CLibro. Observar que hemos indicado que
se sea el elemento raíz del documento XML y se codifique como
Biblioteca:

[XmlRoot("Biblioteca",
Namespace="http://tempuri.org/EsquemaBiblioteca.xsd")]
public class Lista {
[XmlElement]
public CLibro[] Libro = new CLibro[10];
}

Lista lista = new Lista();

El código que permite serializar la clase Lista es el siguiente:

......
XmlSerializer ser = new XmlSerializer (typeof(Lista));
FileStream OStream = new FileStream (filename,
FileMode.Create);

ser.Serialize (OStream, lista);

OStream.Close();
......
Conjuntos de datos

DataSet myDataSet = new DataSet();


...

// -------------------------------------------
// 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");

// Lectura de un documento XML


// ---------------------------

myDataSet.ReadXml("fichero.xml");

// Validación de un documento XML


// ------------------------------

XmlDocument doc = new XmlDocument();


XmlTextReader tr = new XmlTextReader("fichero.xml");
XmlValidatingReader vr = new XmlValidatingReader(tr);
XmlSchemaCollection collection = new XmlSchemaCollection();

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);

// Errores de validación (callback)

private void ValidationEventHandle (object sender,


ValidationEventArgs args)
{
error = true;
Console.WriteLine("\r\n\tError de validación: "
+ args.Message );
}
Programación web

Desarrollo de aplicaciones para Internet

Actualmente, se observa una tendencia a utilizar los estándares de Internet (más


concretamente, la web) para desarrollar aplicaciones de gestión en medianas y grandes
empresas. Este tipo de aplicaciones utiliza software que se descarga de un servidor web, sin
necesidad de instalarlo localmente en cada máquina, lo que se facilita las tareas de
mantenimiento y actualización.

Evolución de las aplicaciones web


HTML estático

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

La creación de aplicaciones web requiere software ejecutándose en el servidor que genere


automáticamente los ficheros HTML que se visualizan en el navegador del cliente. La
comunicación entre el cliente y el servidor se realiza a través de HTTP, un protocolo simple en
el que se establece una conexión TCP independiente para cada par solicitud-respuesta. Esto
implica que el entorno de programación que empleemos debería facilitarnos de alguna forma el
mantenimiento de sesiones de usuario.

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...).

 HTML dinámico (DHTML): Se escriben macros (scripts) que se incrustan en


los ficheros HTML. Usualmente, se utiliza Javascript por cuestiones de
portabilidad, si bien navegadores como el Internet Explorer también
permiten otros lenguajes como VBScript. En HTML dinámico, cada etiqueta
HTML se convierte en un objeto con sus propiedades y eventos asociados.
Los scripts le proporcionan al navegador el código correspondiente a la
respuesta prevista por el programador para los distintos eventos que se
pueden producir.

 ActiveX: Disponible únicamente en el Internet Explorer (al menos, en


principio), es una tecnología basada en COM. Permite desarrollar código
eficiente, si bien su utilización se suele limitar a intranets por cuestiones de
seguridad.
 Applets: Son aplicaciones escritas en Java en las que se distribuye el código
intermedio correspondiente a la máquina virtual Java (bytecodes). Tienen la
ventaja de ser portables (a cualquier plataforma que disponga de un
intérprete de bytecodes) y destacan por su seguridad (cada aplicación se
ejecuta en un espacio independiente [sandbox] que, en principio, no puede
acceder al hardware de la máquina del cliente). Algunos problemas de
rendimiento (y, sobre todo, cuestiones de mercado) han limitado su
utilización.
En el servidor
También existen numerosas alternativas que se pueden utilizar para desarrollar aplicaciones
web en el servidor. Cuando el software se ejecuta en el servidor, éste suele recibir datos
provenientes de URLs, formularios HTML, cookies y cabeceras HTTP. Una vez procesados estos
datos (y tras acceder, posiblemente, a bases de datos, ficheros, mainframes, etc.), en el
servidor se genera dinámicamente una respuesta personalizada en HTML para cada cliente
concreto.

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)
 ...

Ejemplo: ASP y JSP


Las páginas ASP permiten crear aplicaciones web utilizando, usualmente, un lenguaje como
VBScript. Una página ASP contiene HTML estático intercalado con scripts que se encargan de
generar HTML de forma dinámica. JSP funciona de forma análoga, si bien utiliza el lenguaje de
programación Java. Tanto ASP como JSP gozan de gran aceptación a pesar de que no fuerzan
uno de los principios básicos de diseño de software: la separación entre la interfaz de una
aplicación y su lógica interna.
Aplicaciones web con ASP.NET

ASP: Active Server Pages


ASP permite desarrollar aplicaciones para Internet que se ejecutan en el servidor.
Básicamente, desarrollar páginas ASP consiste en intercalar documentos HTML estáticos y
macros dinámicas, mezclando la interfaz con la lógica de la aplicación (lo que, por cierto, no
suele ser demasiado recomendable).

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.

Las limitaciones e inconvenientes mencionados dieron lugar a una serie de cambios en


ASP.NET. ASP.NET, sin embargo, no es completamente compatible con ASP, si bien la mayor
parte de las páginas ASP sólo requieren pequeños cambios para pasarlas a ASP.NET. De
hecho, el primero de los ejemplos funciona correctamente como página ASP.NET (cambiándole
la extensión a .aspx) y el segundo funciona si tenemos en cuenta que en Visual Basic .NET los
paréntesis son obligatorios en las llamadas a métodos.
ASP.NET: Aplicaciones web en la plataforma .NET
ASP.NET es el nombre con el que se conoce la parte de la plataforma .NET que permite el
desarrollo y ejecución tanto de aplicaciones web como de servicios web. Igual que sucedía en
ASP, ASP.NET se ejecuta en el servidor.

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#:

<%@ Page language="c#" %>


<html>
<head>
<title>Hora.aspx</title>
</head>
<script runat="server">
public void Button_Click (object sender, System.EventArgs e)
{
LabelHora.Text = "La hora actual es " + DateTime.Now;
}
</script>
<body>
<form method="post" runat="server">
<asp:Button onclick="Button_Click" runat="server"
Text="Pulse el botón para consultar la hora"/>
<p>
<asp:Label id=LabelHora runat="server" />
</form>
</body>
</html>

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).

El ejemplo anterior ilustra algunas de las características más relevantes de ASP.NET: su


integración en la plataforma .NET, el uso de controles y eventos; y, por último, su
independencia respecto al navegador que utilicemos.

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.

El código se ejecuta en el servidor web en función de los manejadores de eventos que


definamos para los controles y las páginas de nuestra aplicación. Los controles derivan todos
de la clase System.Web.UI.Control y la página web en sí también es un control (derivado
de System.Web.UI.Page, una subclase de System.Web.UI.Control).

ASP.NET se encarga de garantizar la compatibilidad de los controles con los distintos


navegadores que pueda utilizar el usuario:
Además, ASP.NET permite dos estilos para la creación de páginas ASP.NET:

 Incluir tanto controles como código en un fichero .aspx, como hicimos en el


ejemplo (a pesar de que esto nos impide aprovechar muchas de las
ventajas de ASP.NET frente a ASP).
 Mantener los controles en un fichero .aspx y dejar el código aparte (code-
behind page), lo que permite separar físicamente la interfaz de usuario del
código de la aplicación:

En la página ASP.NET (fichero Hora.aspx, Visual Studio 2002/2003):


<%@ Page Language="c#" CodeBehind="Hora.aspx.cs" Inherits="HoraWebForm" %>
<html>
<head>
<title>Hora.aspx</title>
</head>
<body>
<form method="post" runat="server">
<asp:Button id="ButtonHora" runat="server"
Text="Pulse el botón para consultar la hora" />
<p>
<asp:Label id="LabelHora" runat="server" />
</form>
</body>
</html>
En un fichero de código aparte (fichero Hora.aspx.cs, Visual Studio 2002/2003):
public class HoraWebForm : System.Web.UI.Page
{
protected System.Web.UI.WebControls.Button ButtonHora;
protected System.Web.UI.WebControls.Label LabelHora;

override protected void OnInit(EventArgs e)


{
this.ButtonHora.Click += new System.EventHandler(this.ButtonHora_Click);
base.OnInit(e);
}

private void ButtonHora_Click(object sender, System.EventArgs e)


{
LabelHora.Text = "La hora actual es "+DateTime.Now;
}
}
Visual Studio 2003 - Descargar código fuente
Podríamos simplificar algo la implementación del fichero de código asociado a la anterior
página mediante el uso del atributo AutoEventWireup en la directiva <%@ Page ...> con la
que comienza cualquier página ASP.NET.

En ASP.NET 2.0, además, tendremos que utilizar el atributo CodeFile en vez de


CodeBehind, tal como se muestra a continuación:

Fichero Hora.aspx (ASP.NET 2.0, Visual Studio 2005):


<%@ Page Language="C#" AutoEventWireup="true"
CodeFile="Hora.aspx.cs" Inherits="HoraWebForm" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"


"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >


<head runat="server">
<title>Hora.aspx</title>
</head>
<body>
<form id="formHora" runat="server">
<div>
<asp:Button ID="ButtonHora" runat="server"
Text="Pulse el botón para consultar la hora"
OnClick="ButtonHora_Click" />
<br />
<br />
<asp:Label ID="LabelHora" runat="server"></asp:Label>
</div>
</form>
</body>
</html>
Fichero Hora.aspx.cs (ASP.NET 2.0, Visual Studio 2005):
public partial class HoraWebForm : System.Web.UI.Page
{
protected void ButtonHora_Click(object sender, EventArgs e)
{
LabelHora.Text = "La hora actual es " + DateTime.Now;
}
}

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.

Visual Studio 2005 - Descargar código fuente

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;

public class TodayPage:Page


{
protected void OutputDay()
{
Response.Write(DateTime.Now.ToString("D"));
}
}
Código fuente (Visual Studio .NET 2003)
Código fuente (Visual Studio .NET 2005)

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.

Aparte de proporcionar un modelo orientado a objetos de la aplicación, que evita el código


"spaghetti" típico de ASP, los controles web proporcionan compatibilidad automática con
distintos tipos de navegadores (aprovechando la funcionalidad de los navegadores modernos,
como JavaScript o HTML dinámico, sin dejar de funcionar en navegadores más antiguos, los
que se limitan a soportar HTML 3.2). ASP.NET generará el HTML que resulte más apropiado
para el navegador concreto que utilice cada usuario.

Básicamente, existen tres tipos de controles ASP.NET:

 Controles HTML (que representan etiquetas HTML tradicionales).


 Controles web (los controles asociados a las etiquetas ASP.NET).
 Controles de validación (que permiten validar entradas de una forma
cómoda, aunque no siempre resulte la más adecuada).

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;

private void Page_Load(object sender, System.EventArgs e)


{
enlace.HRef = "http://csharp.ikor.org/";
}

override protected void OnInit(EventArgs e)


{
this.Load += new System.EventHandler(this.Page_Load);
base.OnInit(e);
}
}

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.

Código fuente (Visual Studio .NET 2003)


Código fuente (Visual Studio .NET 2005)

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:

<asp:control id="identificador" runat="server" />

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)

Como se puede comprobar, el desarrollo de aplicaciones web con controles ASP.NET es


completamente análogo al desarrollo de aplicaciones Windows. Sólo tenemos que seleccionar
los controles adecuados para nuestra interfaz e implementar la respuesta de nuestra aplicación
a los eventos que deseemos controlar.

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:

Sólo tenemos que establecer la propiedad ControlToValidate (igual


al TextBox que usamos como entrada), el mensaje de error que se
mostrará (ErrorMessage) y la condición que ha de verificar la
entrada (en este caso, su tipo, Type=Integer, y el rango de valores
permitido, entre MinimumValue y MaximumValue).

Código fuente (Visual Studio .NET 2003)


Código fuente (Visual Studio .NET 2005)
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.

Obviamente, la validación la podríamos haber realizado implementando la respuesta de


nuestra aplicación a los eventos asociados utilizando controles web estándar, aunque de esta
forma, ASP.NET se encargará de generar automáticamente las rutinas de validación. En el caso
de que nuestro navegador acepte JavaScript, por ejemplo, la validación se realizará en el
cliente, disminuyendo de esta forma la carga que ha de soportar el servidor de aplicaciones
web.

Control de validación Descripción


Compara el valor de una entrada con el
CompareValidator
de otra o un valor fijo.
Permite implementar un método
CustomValidator cualquiera que maneje la validación del
valor introducido.
Comprueba que la entrada esté entre dos
RangeValidator
valores dados.
Valida el valor de acuerdo a un patrón
RegularExpressionValidator
establecido como una expresión regular.
Hace que un valor de entrada sea
RequiredFieldValidator
obligatorio.
Muestra un informe con todos los errores
ValidationSummary
de validación de la página.

Controles definidos por el usuario

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 .ascx se define la presentación visual del control, como si se tratase


de una páginas .aspx, salvo que la directiva que aparece al comienzo del fichero es
@Control:

<%@ Control Language="c#" AutoEventWireup="false"


Codebehind="ContactViewer.ascx.cs"...

En el fichero de código .ascx.cs definimos una subclase de


System.Web.UI.UserControl, con todas las propiedades que deseemos:

public class ContactViewer : System.Web.UI.UserControl


{
protected System.Web.UI.WebControls.Label LabelAddress;
protected System.Web.UI.WebControls.Label LabelFax;
protected System.Web.UI.WebControls.Label LabelMobile;
protected System.Web.UI.WebControls.Label LabelTelephone;
protected System.Web.UI.WebControls.Image ImagePhoto;
protected System.Web.UI.WebControls.Label LabelName;
protected System.Web.UI.WebControls.Label LabelComments;
protected System.Web.UI.WebControls.HyperLink LinkEMail;

public Contact DisplayedContact {


get { return (Contact) ViewState["contact"]; }
set { ViewState["contact"] = value; UpdateUI(); }
}

private void UpdateUI () {


Contact contact = (Contact) ViewState["contact"];
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:

<%@ Register TagPrefix="user" TagName="ContactViewer"


Src="ContactViewer.ascx" %>
...
<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:

Agenda de contactos: Código fuente de la aplicación (Visual Studio 2003)


Agenda de contactos: Código fuente de la aplicación (Visual Studio 2005, como "Aplicación
Web ASP.NET")
Agenda de contactos: Código fuente de la aplicación (Visual Studio 2005, como "Sitio Web")
Funcionamiento de las páginas ASP.NET
En una página ASP.NET, todos los controles cuyo funcionamiento haya de controlarse en el
servidor deben estar incluidos dentro de nu formulario HTML (etiqueta <form>) con el atributo
runat="server", el cual le indica al IIS (o al servidor de desarrollo del Visual Studio) que el
formulario ha de procesarse en el servidor antes de enviárselo al cliente:

<form runat="server">

... HTML y controles ASP.NET ...

</form>

Este formulario, que ha de ser necesariamente único, es el que se encarga de facilitar la


interacción del servidor con el cliente. De hecho, las páginas ASP.NET funcionan de una forma
similar a los formularios Windows, si bien el modo de interacción de las interfaces web
introduce algunas limitaciones (al ser cada solicitud recibida por el servidor independiente de
las anteriores).

Postbacks

Al solicitar una página ASP.NET desde un cliente, en el servidor se dispara el evento


Page_Load asociado a la página antes de generar ninguna salida. Es en este evento donde
podemos realizar las tareas de inicialización de la página (rellenado de listas de valores,
establecimiento de valores por defecto...).

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).

 En ASP clásico, al enviar un formulario, todos sus valores se vacían, por lo


que, si se ha producido un pequeño error en uno de los valores, tendremos
que programar nosotros el código que se encargue de rellenar los valores
que sí eran correctos (siempre y cuando queramos evitarle al usuario tener
que introducir de nuevo todos los datos, claro está). Esto resulta bastante
tedioso y propenso a errores.
 En ASP.NET, el formulario reaparece en el navegador del cliente con los
valores que tuviese en el momento de enviarlo al servidor.

El estado de la página se define mediante un campo oculto denominado __VIEWSTATE que se


le añade a cada página que tenga un formulario con el atributo runat="server", de forma
que es el cliente el que se encarga de mantener el estado. Si visualizamos el código fuente de
la página HTML que se muestra en el cliente, podemos ver algo así:
<input type="hidden" name="__VIEWSTATE"

value="dDwtMjEwNjQ1OTkwMDs7PiTPnxCh1VBUIX3K2htmyD8Dq6oq" />

El mantenimiento del estado de la página es automático salvo que explícitamente indiquemos


lo contrario:

 A nivel de la página, podemos emplear la directiva <%@ Page


 EnableViewState="false" %> en la cabecera del fichero .aspx.
 A nivel de un control particular, podemos establecer su propiedad
EnableViewState a false.

Antes teníamos que haber escrito algo como...


<%@ Page language="c#" %>
<html>
<body>
<form runat="server" method="post">
Tu nombre: <input type="text" name="nombre" size="20">
<input type="submit" value="Enviar">
</form>
<%
string name = Request.Form["nombre"];

if (name!=null && name!="") {


Response.Write("Hola, " + name + "!");
}
%>
</body>
</html>

En ASP.NET, no obstante, podemos teclear lo siguiente y evitar que el valor


introducido desaparezca:

<%@ Page language="c#" %>


<html>
<script runat="server">
void enviar (object sender, EventArgs e)
{
label.Text = "Hola, " + textbox.Text + "!";
}
</script>
<body>
<form runat="server" method="post">
Tu nombre: <asp:TextBox id="textbox" runat="server" />
<asp:Button OnClick="enviar" Text="Enviar" runat="server" />
<p><asp:Label id="label" runat="server" /></p>
</form>
</body>
</html>
Código fuente
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;

public class TodayPage:Page


{
protected void OutputDay()
{
Response.Write(DateTime.Now.ToString("D"));
}
}
Código fuente (Visual Studio .NET 2003)
Código fuente (Visual Studio .NET 2005)

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.

Aparte de proporcionar un modelo orientado a objetos de la aplicación, que evita el código


"spaghetti" típico de ASP, los controles web proporcionan compatibilidad automática con
distintos tipos de navegadores (aprovechando la funcionalidad de los navegadores modernos,
como JavaScript o HTML dinámico, sin dejar de funcionar en navegadores más antiguos, los
que se limitan a soportar HTML 3.2). ASP.NET generará el HTML que resulte más apropiado
para el navegador concreto que utilice cada usuario.

Básicamente, existen tres tipos de controles ASP.NET:

 Controles HTML (que representan etiquetas HTML tradicionales).


 Controles web (los controles asociados a las etiquetas ASP.NET).
 Controles de validación (que permiten validar entradas de una forma
cómoda, aunque no siempre resulte la más adecuada).

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;

private void Page_Load(object sender, System.EventArgs e)


{
enlace.HRef = "http://csharp.ikor.org/";
}

override protected void OnInit(EventArgs e)


{
this.Load += new System.EventHandler(this.Page_Load);
base.OnInit(e);
}
}

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.

Código fuente (Visual Studio .NET 2003)


Código fuente (Visual Studio .NET 2005)

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:

<asp:control id="identificador" runat="server" />

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)

Como se puede comprobar, el desarrollo de aplicaciones web con controles ASP.NET es


completamente análogo al desarrollo de aplicaciones Windows. Sólo tenemos que seleccionar
los controles adecuados para nuestra interfaz e implementar la respuesta de nuestra aplicación
a los eventos que deseemos controlar.

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:

Sólo tenemos que establecer la propiedad ControlToValidate (igual


al TextBox que usamos como entrada), el mensaje de error que se
mostrará (ErrorMessage) y la condición que ha de verificar la
entrada (en este caso, su tipo, Type=Integer, y el rango de valores
permitido, entre MinimumValue y MaximumValue).

Código fuente (Visual Studio .NET 2003)


Código fuente (Visual Studio .NET 2005)

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.

Obviamente, la validación la podríamos haber realizado implementando la respuesta de


nuestra aplicación a los eventos asociados utilizando controles web estándar, aunque de esta
forma, ASP.NET se encargará de generar automáticamente las rutinas de validación. En el caso
de que nuestro navegador acepte JavaScript, por ejemplo, la validación se realizará en el
cliente, disminuyendo de esta forma la carga que ha de soportar el servidor de aplicaciones
web.

Control de validación Descripción


Compara el valor de una entrada con el de otra o un
CompareValidator
valor fijo.
Permite implementar un método cualquiera que maneje
CustomValidator
la validación del valor introducido.
RangeValidator Comprueba que la entrada esté entre dos valores dados.
Valida el valor de acuerdo a un patrón establecido como
RegularExpressionValidator
una expresión regular.
RequiredFieldValidator Hace que un valor de entrada sea obligatorio.
Muestra un informe con todos los errores de validación
ValidationSummary
de la página.

Controles definidos por el usuario

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 .ascx se define la presentación visual del control, como si se tratase


de una páginas .aspx, salvo que la directiva que aparece al comienzo del fichero es
@Control:

<%@ Control Language="c#" AutoEventWireup="false"


Codebehind="ContactViewer.ascx.cs"...

En el fichero de
código .ascx.cs definimos una subclase de
System.Web.UI.UserControl, con todas las propiedades que deseemos:

public class ContactViewer : System.Web.UI.UserControl


{
protected System.Web.UI.WebControls.Label LabelAddress;
protected System.Web.UI.WebControls.Label LabelFax;
protected System.Web.UI.WebControls.Label LabelMobile;
protected System.Web.UI.WebControls.Label LabelTelephone;
protected System.Web.UI.WebControls.Image ImagePhoto;
protected System.Web.UI.WebControls.Label LabelName;
protected System.Web.UI.WebControls.Label LabelComments;
protected System.Web.UI.WebControls.HyperLink LinkEMail;

public Contact DisplayedContact {


get { return (Contact) ViewState["contact"]; }
set { ViewState["contact"] = value; UpdateUI(); }
}

private void UpdateUI () {


Contact contact = (Contact) ViewState["contact"];

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:

<%@ Register TagPrefix="user" TagName="ContactViewer"


Src="ContactViewer.ascx" %>
...

<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:

Agenda de contactos: Código fuente de la aplicación (Visual Studio 2003)


Agenda de contactos: Código fuente de la aplicación (Visual Studio 2005, como "Aplicación
Web ASP.NET")
Agenda de contactos: Código fuente de la aplicación (Visual Studio 2005, como "Sitio Web")
Funcionamiento de las páginas ASP.NET
En una página ASP.NET, todos los controles cuyo funcionamiento haya de controlarse en el
servidor deben estar incluidos dentro de nu formulario HTML (etiqueta <form>) con el atributo
runat="server", el cual le indica al IIS (o al servidor de desarrollo del Visual Studio) que el
formulario ha de procesarse en el servidor antes de enviárselo al cliente:

<form runat="server">

... HTML y controles ASP.NET ...

</form>

Este formulario, que ha de ser necesariamente único, es el que se encarga de facilitar la


interacción del servidor con el cliente. De hecho, las páginas ASP.NET funcionan de una forma
similar a los formularios Windows, si bien el modo de interacción de las interfaces web
introduce algunas limitaciones (al ser cada solicitud recibida por el servidor independiente de
las anteriores).

Postbacks

Al solicitar una página ASP.NET desde un cliente, en el servidor se dispara el evento


Page_Load asociado a la página antes de generar ninguna salida. Es en este evento donde
podemos realizar las tareas de inicialización de la página (rellenado de listas de valores,
establecimiento de valores por defecto...).

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).

 En ASP clásico, al enviar un formulario, todos sus valores se vacían, por lo


que, si se ha producido un pequeño error en uno de los valores, tendremos
que programar nosotros el código que se encargue de rellenar los valores
que sí eran correctos (siempre y cuando queramos evitarle al usuario tener
que introducir de nuevo todos los datos, claro está). Esto resulta bastante
tedioso y propenso a errores.
 En ASP.NET, el formulario reaparece en el navegador del cliente con los
valores que tuviese en el momento de enviarlo al servidor.

El estado de la página se define mediante un campo oculto denominado __VIEWSTATE que se


le añade a cada página que tenga un formulario con el atributo runat="server", de forma
que es el cliente el que se encarga de mantener el estado. Si visualizamos el código fuente de
la página HTML que se muestra en el cliente, podemos ver algo así:
<input type="hidden" name="__VIEWSTATE"

value="dDwtMjEwNjQ1OTkwMDs7PiTPnxCh1VBUIX3K2htmyD8Dq6oq" />

El mantenimiento del estado de la página es automático salvo que explícitamente indiquemos


lo contrario:

 A nivel de la página, podemos emplear la directiva <%@ Page


 EnableViewState="false" %> en la cabecera del fichero .aspx.
 A nivel de un control particular, podemos establecer su propiedad
EnableViewState a false.

Antes teníamos que haber escrito algo como...


<%@ Page language="c#" %>
<html>
<body>
<form runat="server" method="post">
Tu nombre: <input type="text" name="nombre" size="20">
<input type="submit" value="Enviar">
</form>
<%
string name = Request.Form["nombre"];

if (name!=null && name!="") {


Response.Write("Hola, " + name + "!");
}
%>
</body>
</html>

En ASP.NET, no obstante, podemos teclear lo siguiente y evitar que el valor introducido


desaparezca:

<%@ Page language="c#" %>


<html>
<script runat="server">
void enviar (object sender, EventArgs e)
{
label.Text = "Hola, " + textbox.Text + "!";
}
</script>
<body>
<form runat="server" method="post">
Tu nombre: <asp:TextBox id="textbox" runat="server" />
<asp:Button OnClick="enviar" Text="Enviar" runat="server" />
<p><asp:Label id="label" runat="server" /></p>
</form>
</body>
</html>
Código fuente
Sesiones de usuario

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:

valor) y un texto como cuerpo del mensaje.

El formato de una solicitud es de la siguiente forma:

GET http://elvex.ugr.es/index.html
If-Modified-Since: Thu, 31 Oct 2002 19:41:00 GMT

La primera línea de la solicitud, aparte de indicar la versión de HTTP utilizada, también


determina el método utilizado para acceder al recurso identificado mediante un URI [Universal
Resource Identifier], tal como se define en el RFC 2396. Los métodos más usados son GET y
POST (que sólo se diferencian en la forma de pasar los parámetros de los formularios), así
como HEAD (que sólo devuelve metadatos acerca del recurso solicitado).

La respuesta del servidor será del tipo:

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>

Como se puede apreciar, en la primera línea de la respuesta aparece la versión de HTTP


empleada, un código de estado de tres dígitos y una descripción de ese código de estado. La
siguiente tabla resume las distintas categorías de los códigos devueltos en las respuestas
HTTP:

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

Application Variables globales a nivel de la aplicación (comunes a


todas las solicitudes recibidas desde cualquier cliente)
Variables globales a nivel de una sesión de usuario
Session (comunes a todas las solicitudes de un cliente
concreto)

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:

 Manualmente en el cliente, usando cookies a bajo nivel (una solución no


demasiado buena).
 Manualmente en el servidor, usando ficheros o, preferiblemante, bases de
datos (la solución óptima en entornos distribuidos y sistemas tolerantes a
fallos).
 Automáticamente, usando las colecciones Session y Application
facilitadas por ASP.NET. Dichas colecciones las podemos inicializar
utilizando los métodos Session_Start y Application_Start del fichero
Global.asax asociado a la aplicación web (o su fichero de código asociado
Global.asax.cs).

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.

El identificador de la sesión se puede transmitir utilizando la cookie


ASP.NET_SessionId:
cqcgvjvjirmizirpld0dyi5
o sin emplear cookies, incluyendo el identificador en la URL:

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:

Inicialización en el fichero Global.asax (o Global.asax.cs):


void Session_Start()
{
Session["UserName"] = "";
}
En la página de entrada a la aplicación:
void Page_Load (Object Src, EventArgs e)
{
Session["UserName"] = TextBoxUser.Text;
}
Desde las páginas internas de la aplicación:
labelUser.Text = (string) Session["UserName"];

NOTA: El fichero Global.asax.cs se crea automáticamente en Visual Studio 2003


cuando creamos un proyecto de tipo "aplicación web ASP.NET". En Visual Studio
2005 hemos de crearlo explícitamente añadiendo un nuevo elemento de tipo "Clase
de aplicación global" a nuestro proyecto web. En el caso de un "sitio web", se creará
un único fichero Global.asax en el que incluiremos el código correspondiente a los
eventos globales de la aplicación, mientras que se crearán dos ficheros
independientes (Global.asax y Global.asax.cs) si nuestro proyecto es de tipo
"aplicación web 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):

protected void Application_Start(Object sender, EventArgs e)


{
Application["contador"] = 0;
}

protected void Session_Start(Object sender, EventArgs e)


{
Session["contador"] = 0;
}

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):

protected System.Web.UI.WebControls.Label visitas;

private void Page_Load(object sender, System.EventArgs e)


{
Application["contador"] = (int)Application["contador"] + 1;
Session["contador"] = (int)Session["contador"] + 1;

visitas.Text = Application["contador"] + " hits"


+ " (" + Session["contador"] + " del usuario
actual)";
}
Código fuente del ejemplo para Visual Studio 2003
Código fuente del ejemplo para Visual Studio 2005

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.

Alternativas para gestionar las sesiones de usuario en ASP.NET


 InProc: En el proceso del servidor web (aspnet_wp.exe).

 StateServer: En un proceso aparte compartido entre varios servidores web


(aspnet_state.exe).

 SqlServer: En un servidor SQL*Server.

La selección de una u otra alternativa implicará un compromiso entre la eficiencia y la


fiabilidad en la gestión de sesiones para nuestra aplicación web.
Seguridad en .NET

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.

En el directorio de una aplicación web se incluye un fichero XML de configuración llamado


Web.config, cuya estructura se corresponde con la del siguiente ejemplo:

<configuration>
<system.web>

<authentication mode="Forms">
<forms loginUrl="login.aspx" name=".ASPXFORMSAUTH"></forms>
</authentication>

<authorization>
<deny users="?" />
</authorization>

</system.web>
</configuration>

La sección de autentificación (authentication) se utiliza para establecer la política de


identificación de usuarios que utilizará nuestra aplicación:

 Forms se emplea para utilizar formularios de autentificación en los que


seremos nosotros los que decidamos quién accede a nuestra aplicación.
 Passport permite que nuestra aplicación utilice el sistema de autentificación
Passport de Microsoft (más información en http://www.passport.com).
 Windows se utiliza para delegar en el sistema operativo las tareas de
autentificación de usuarios, con lo cual sólo podrán acceder a nuestra
aplicación los usuarios que existan en nuestro sistema.
 Finalmente, None deshabilita los mecanismos de autentificación, con lo que
cualquiera puede acceder a ella desde cualquier lugar del mundo sin
ninguna restricción de acceso.

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.

A continuación, la sección de autorización ( authorization) se utiliza, en el mismo ejemplo,


para restringir el acceso a los usuarios no identificados.

Un fichero Web.config restringe el acceso a un directorio y a todos sus subdirectorios, si bien


en los subdirectorios se pueden incluir otros ficheros Web.config que redefinan las
restricciones de acceso a los subdirectorios del directorio principal de nuestra aplicación web.

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>

Si lo que quisiéramos es restringir el acceso a un usuario particular, sólo rendríamos que


incluir la siguiente sección de autorización en nuestro fichero Web.config:

<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:

Si tras varios intentos no introducimos un nombre de usuario válido y su


contraseña correcta, el servidor web nos devolverá un error de autentificación
"HTTP 401.3 - Access denied by ACL on resource":
Ejemplo de autentificación mediante el sistema operativo (Visual Studio 2003)
Ejemplo de autentificación mediante el sistema operativo (Visual Studio 2005)
Formularios de autentificación en aplicaciones web
ASP.NET
En determinadas ocasiones no nos podemos permitir el lujo de crear un usuario en el sistema
operativo para cada usuario que pueda acceder a nuestra aplicación, por lo que seremos
nosotros los encargados de gestionar los usuarios y el acceso de éstos a las distintas partes de
nuestras aplicaciones.

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.

Cuando el usuario no introduzca una clave de acceso correcta o su identificador no exista en


nuestra base de datos de usuarios registrados, podemos mostrarle un mensaje de error
informativo mediante una etiqueta (labelMessage.Text=...) o redirigir al usuario a la
página que nos interese utilizando el método Response.Redirect("http://...");. Esto
último lo podríamos utilizar si estamos construyendo un sistema en el cual permitimos que los
usuarios puedan registrarse ellos mismos.

Creamos una aplicación web con dos formularios, uno de los cuales lo utilizaremos
como formulario de identificación:

En primer lugar, debemos configurar correctamente el fichero Web.config, tal como


se muestra a continuación:

<authentication mode="Forms">
<forms loginUrl="Login.aspx" name=".ASPXFORMSAUTH"></forms>
</authentication>

<authorization>
<deny users="?" />
</authorization>

Al intentar acceder a cualquier formulario de nuestra aplicación, si no estamos


identificados se nos redirecciona al formulario de login:

Si no introducimos incorrectamente la clave de acceso, se nos debería mostrar un


mensaje informativo de error:
Obviamente, en una aplicación real no deberíamos ser tan explícitos ;-)

Sólo cuando nos identifiquemos correctamente podremos acceder a la aplicación:

Ejemplo de autentificación mediante formularios web (Visual Studio 2003)


Ejemplo de autentificación mediante formularios web (Visual Studio 2005)

IMPORTANTE: Con lo que hemos visto, conseguimos controlar el acceso a nuestras


aplicaciones web, aunque éstas aún no son realmente seguras porque los datos se transmiten
tal cual, sin encriptar. Para proteger los datos que se transmiten entre el servidor web y los
clientes que acceden a él debemos configurar el servidor web para que pueda utilizar el
protocolo HTTPS. Este protocolo utiliza técnicas criptográficas de clave pública y nos hará falta
instalar un certificado (un par clave pública-clave privada) para que podamos acceder al
servidor usando https://...
HTML

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].

Estructura de los documentos HTML

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:

 La cabecera (entre <HEAD> y </HEAD>) contiene información general sobre el


documento que no se muestra en pantalla: título, autor, descripción...
 Las etiquetas <BODY> y </BODY> definen la parte principal o cuerpo del documento.

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>

El cuerpo del documento puede incluir, entre otros elementos:

 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>

Con el fin de personalizar la presentación de la información, HTML incluye:

 <B> ... </B> pone el texto en negrita.


 <I> ... </I> pone el texto en cursiva.
 <U> ... </U> subraya el texto.
 <CENTER> ... </CENTER> sirve para centrar el texto.
 <FONT SIZE="+1" COLOR="#rrggb"> ... </FONT> se emplea para cambiar el
tamaño del tipo de letra (p.ej. +2 +1 -1 -2) y su color representado en hexadecimal
utilizando RGB: #000000 (negro), #ffffff (blanco), #ff0000 (rojo), #00ff00 (verde),
#0000ff (azul)...
 <BR> introduce saltos de línea.
 <HR> introduce una línea horizontal a modo de separador.

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:

Carácter Secuencia HTML


& &amp;
< &lt;
> &gt;
" &quot;
© &copy;
® &reg;
™ &trade;
€ &euro;
á &aacute;
è &egrave;
ê &ecirc;
ü &uuml;
ß &szlig;
Ø &Oslash;
æ &aelig;
ñ &ntilde;
Ñ &Ntilde;

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>

aparece en el navegador como:

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:[email protected]">
<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>

quedaría como se muestra a continuación dentro de una tabla:

Nombre

Comentarios
Enviar sugerencias por e-mail

En los formularios HTML se pueden introducir:

 Cuadros de texto (<INPUT TYPE="TEXT"...>).


 Cuadros de texto que no muestran lo que el usuario escribe (<INPUT
 TYPE="PASSWORD"...>).
 Textos de varias líneas (<TEXTAREA ...> ...
 </TEXTAREA>).
 Opciones (<INPUT TYPE="CHECKBOX"...>).
 Opciones mutuamente excluyentes (<INPUT
 TYPE="RADIO"...>).
 Listas para seleccionar valores (<SELECT> <OPTION>
 <OPTGROUP>).
 Ficheros adjuntos (<INPUT TYPE="FILE"...>).
 Parámetros ocultos para el usuario (<INPUT
 TYPE="HIDDEN"...>).

Para enviar los datos del formulario a la URL especificada en FORM

ACTION, se puede utilizar un botón (<INPUT

TYPE="SUBMIT"...>) o emplear una imagen (<INPUT

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 facilitar el mantenimiento de nuestras páginas HTML y asegurarnos de que se mantiene


cierta coherencia en su presentación de cara al usuario, podemos utilizar hojas de estilo CSS
(Cascading Style Sheets).

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:

<link REL=STYLESHEET TYPE="text/css"


HREF="style.css">

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

 W3C: World Wide Web Consortium


 Index DOT (CSS & HTML)
Concurrencia

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.

Un proceso es un programa en ejecución con un estado asociado. Las distintas aplicaciones


que se ejecutan en un sistema operativo multitarea son procesos independientes. Cada una de
ellas tiene asociado un contexto (prioridad, estado del procesador, lista de interrupciones y
señales que admite, páginas de memoria que ocupa, etc.).

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.

Hebras o procesos independientes pueden encargarse de realizar las operaciones costosas


mientras que la hebra principal de la aplicación sigue gestionando los eventos procedientes de
la interfaz de usuario.

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).

Aprovechamiento de los recursos del sistema

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:

 Se puede simplificar la implementación de un sistema si una secuencia


compleja de operaciones se puede descomponer en una serie de tareas
independientes que se ejecuten concurrentemente.
 Tareas independientes se implementan por separado (menor acoplamiento
entre las distintas partes del sistema)
 Un diseño modular facilita la incorporación de futuras ampliaciones en
nuestras aplicaciones.

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).

El método Process.Start() equivale a la llamada ShellExecute del API de Windows


(Win32) y es el que deberemos utilizar para lanzar un proceso. Los parámetros del proceso se
especifican mediante un objeto de la clase ProcessStartInfo. Al encapsular una llamada al
shell de Windows, el método Start también podemos usarlo para abrir un fichero de cualquier
tipo de los que tengan acciones asociadas en el registro de Windows.

Ejecución de procesos

Probar las distintas formas de lanzar un proceso escribiendo la respuesta al evento


Click de cada botón de la ventana que se muestra en la imagen de encima:

Lanzar proceso:

System.Diagnostics.Process.Start("iexplore.exe");

Lanzar proceso con parámetros:

string proceso = "iexplore.exe";


string args = "http://elvex.ugr.es/decsai/Csharp/";

System.Diagnostics.Process.Start("iexplore.exe", args);

Lanzar un proceso utilizando ProcessStartInfo:

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);

Lanzar el proceso adecuado para un fichero cualquiera:

OpenFileDialog openFileDialog = new OpenFileDialog();

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);
}

Abrir una URL:

System.Diagnostics.Process.Start("http://elvex.ugr.es/decsai/Csharp/");

Uso de los verbos del shell:

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)

RECOMENDACIÓN: Cuando se utiliza el método Process.Start, también se debería llamar


siempre al método Process.Close para liberar la memoria asociada al objeto de tipo
Process.
Finalización de procesos
En los ejemplos anteriores, si quisiéramos que nuestra aplicación detuviese su ejecución hasta
que el proceso lanzado finalizase su ejecución, sólo tendríamos que llamar al método
WaitForExit:

Process proceso = Process.Start(...);

proceso.WaitForExit();
proceso.Close();

La espera podría hacerse por un período de tiempo limitado si utilizamos un parámetro en la


llamada al método WaitForExit:

Ejecución invisible de comandos MS-DOS

Process proceso = new Process()

string salida = Application.StartupPath + "/output.txt";


string path = System.Environment.GetFolderPath
(Environment.SpecialFolder.System);

proceso.StartInfo.FileName = "cmd.exe";
proceso.StartInfo.Arguments =
"/C dir \""+path+"\" >> \"" + salida +"\" && exit";

proceso.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
proceso.StartInfo.CreateNoWindow = true;
proceso.Start()

proceso.WaitForExit(1000); // 1 segundo de timeout

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:

Evento asociado a la terminación de un proceso

Process proceso = new Process()


...
proceso.Exited += new EventHandler(ExitHandler);
proceso.Start()
...

El método ExitHandler se encarga de realizar las tareas necesarias al finalizar


la ejecución del proceso, como por ejemplo:

public void ExitHandler(object sender, EventArgs e)


{
Process proceso = (Process) sender;

MessageBox.Show ( "PID: " + proceso.Id


+ System.Environment.NewLine
+ "Hora: " + proceso.ExitTime
+ System.Environment.NewLine
+ "Código: " + proceso.ExitCode );

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.

En la siguiente sección veremos cómo se puede forzar la finalización de un proceso y también


aprenderemos a acceder a la información relativa a los procesos que se estén ejecutando en
una máquina Windows (la misma información que figura en el Administrador de Tareas de
Windows).
Monitorización de procesos

Monitor de procesos

Creamos una aplicación Windows a la que denominamos WindowMonitor, cuyo


formulario principal (Text="Procesos del sistema") contiene los siguientes componentes:

 ListBox listProcesses (ScrollAlwaysVisible=true).


 TextBox textInfo (BackColor=Info; Multiline=true).
 Button buttonClose (Text="WindowClose"; ForeColor=ForestGreen).
 Button buttonKill (Text="Kill"; ForeColor=DarkRed).
 Label labelWarning (TextAlign=MiddleCenter; Text="¡OJO! Kill puede
ocasionar la pérdida de datos al forzar la terminación de un proceso...").

Al cargar el formulario, rellenamos la lista de procesos del sistema (evento FormLoad):

Process[] procesos = Process.GetProcesses();

listProcesses.Items.AddRange(procesos);

Cuando el usuario selecciona un proceso concreto, mostramos información detallada


acerca del proceso seleccionado (evento SelectedIndexChanged del ListBox):

Process selected = (Process) listProcesses.SelectedItem;

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
};

Finalmente, programamos la respuesta de la aplicación correspondiente a los dos


botones de nuestra interfaz:

// Fuerza la finalización del proceso


// (puede provocar la pérdida de datos)

private void buttonKill_Click(object sender, System.EventArgs e)


{
Process selected = (Process) listProcesses.SelectedItem;

if (selected!=null)
selected.Kill();
}

// Solicita la terminación de un proceso


// (como si el usuario solicitase cerrar la aplicación)

private void buttonClose_Click(object sender, System.EventArgs e)


{
Process selected = (Process) listProcesses.SelectedItem;

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)

NOTA: En la barra de herramientas [toolbox], dentro del apartado "Components", podemos


encontrar un componente denominado Process que nos permite trabajar con procesos de
forma visual.
Operaciones de E/S
En ocasiones nos interesa que la entrada de un proceso provenga directamente de la salida de
otro proceso. Enviar la salida a un fichero y después leer el fichero no siempre es la mejor
opción, ya que el uso de "pipes" resulta mucho más eficiente para conectar distintos procesos.
La clase Process nos permite redireccionar los canales de E/S estándar (StdIn, StdOut y
StdErr). Sólo tenemos que fijar a "true" las propiedades RedirectStandardInput,
RedirectStandardOutput y RedirectStandardError, tras lo cual podemos acceder a las
propiedades StandardInput, StandardOutput y StandardError del proceso (objetos de tipo
StreamReader y StreamWriter).

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.

Process proceso = new Process();

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()

StreamWriter sIn = proceso.StandardInput;


sIn.AutoFlush = true;

StreamReader sOut = proceso.StandardOutput;


StreamReader sErr = proceso.StandardError;

sIn.Write("dir c:\" + System.Environment.NewLine);


sIn.Write("exit" + System.Environment.NewLine);

string output = sOut.ReadToEnd();

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:

Acceso al Bloc de Notas con SendKeys

Process proceso = new Process();

proceso.StartInfo.FileName = "notepad";
proceso.StartInfo.WindowStyle = ProcessWindowStyle.Normal;
proceso.EnableRaisingEvents = true;
proceso.Start();

proceso.WaitForInputIdle(1000); // 1 segundo de timeout


if (proceso.Responding)
System.Windows.Forms.SendKeys.SendWait
("Texto enviado con System.Windows.Forms.SendKeys");
Descargar código fuente (Visual Studio 2003)
Descargar código fuente (Visual Studio 2005)

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).

No se pueden utilizar SendKeys hasta que no se haya creado y se visualice la ventana


principal de la aplicación, por lo que en el ejemplo de arriba se empleó el método
Process.WaitForInputIdle, que espera hasta que la aplicación pueda aceptar entradas
por parte del usuario y cuyo funcionamiento es análogo a Process.WaitForExit.

Acceso a recursos nativos de Windows


Con SendKeys se puede simular la pulsación de cualquier combinación de teclas. No
obstante, SendKeys se limita a enviar eventos de pulsación de teclas a la aplicación que
esté activa en cada momento. Y no existe ninguna función estándar en .NET que nos
permita establecer la aplicación activa, pero sí en el API nativo de Windows.

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);

Una vez hecho esto, ya podemos acceder a la función SetForegroundWindow como si de


un método más se tratase:

Process proceso = ...

if (proceso.Responding) {
SetForegroundWindow(proceso.MainWindowHandle);
SendKeys.SendWait("Sorpresa!!!");
SetForegroundWindow(this.Handle);
}

Y ya podemos hacer prácticamente cualquier cosa con una aplicación Windows,


controlándola desde nuestros propios programas.

Descargar código fuente (Visual Studio 2003)


Descargar código fuente (Visual Studio 2005)
Hebras

Todas las aplicaciones .NET son en realidad aplicaciones multihebra.

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.

// Creación del objeto que aloja nuestra hebra

Tarea tarea = new Tarea();

// Inicialización de parámetros

tarea.parameter= 1234;

// Establecimiento del punto de entrada de la hebra (delegado)

ThreadStart threadStart = new ThreadStart(tarea.Run);

// Creación de la hebra

Thread thread = new Thread(threadStart);

// Ejecución de la hebra

thread.Start();

// ...

// Espera a la finalización de la hebra

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...

Comenzamos creando una aplicación Windows con un formulario principal como el


siguiente:
 Formulario FormPI (Text="Cálculo de PI").
 NumericUpDown editDigits (Align=Right; Maximum=10000; Value=1000).
 Button buttonCalc (Text="Calcular").
 TextBox textPI (Text=""; Multiline=true).
 ProgressBar progress.

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));

ShowProgress(pi.ToString(), calculados+nuevos, precision);


calculados += 9;
}
}
Los avances en el cálculo de PI los mostramos periódicamente en el TextBox y, para
que el usuario tenga una idea de cuánto queda, también en la barra de progreso de
nuestro formulario:
void ShowProgress (string pi, int actual, int total)
{
textPI.Text = pi;
progress.Maximum = total;
progress.Value = actual;
}
La forma ingenua de implementar nuestro aplicación sería hacer que, al pulsar el
botón, se calculase el valor de PI con la precisión solicitada. No obstante, aparte de
que nuestra aplicación se queda bloqueada mientras se realiza el cálculo, al cambiar
a otra aplicación y luego volver a la nuestra, nos podríamos encontrar una
desagradable sorpresa (muy común, por otro lado, en demasiadas aplicaciones
comerciales):
La imagen anterior se debe a que nuestra aplicación Windows, ocupada en realizar el
cálculo de PI como respuesta a la pulsación del botón, no atiende ningún otro evento
de los que se producen (como, por ejemplo, el que le pide refrescar su imagen en
pantalla: el evento Paint del formulario). La solución a nuestro problema pasa,
pues, por crear una hebra independiente que se ejecute en paralelo y no interfiera en
el comportamiento habitual del interfaz de nuestra aplicación:

private void buttonCalc_Click(object sender, System.EventArgs e)


{
ThreadStart piThreadStart = new ThreadStart(PiStart);
Thread piThread = new Thread(piThreadStart);

piThread.Start();
}

private void PiStart ()


{
CalcularPi ( (int) editDigits.Value );
}
Con esta pequeña modificación conseguimos que el interfaz gráfico de nuestra
aplicación funcione correctamente:
Ejecución asíncrona de delegados
Para no tener que crear una función sin parámetros especialmente escrita para poder lanzar la
hebra (y tener que implementar en ella la inicialización de los parámetros reales de la hebra),
podemos utilizar delegados:

// Declaración

delegate void PiDelegate (int precision);

// Creación

PiDelegate delegado = new PiDelegate(CalcularPi);

// Uso

delegado((int)editDigits.Value);

El delegado declarado deriva de la clase MultiCastDelegate, que implementa tres


funciones:

class PiDelegate : MulticastDelegate


{
public void Invoke(int precision);
public void BeginInvoke(int precision, AsyncCallback
callback, object state);
public void EndInvoke(IAsyncResult result);
}

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:

Cálculo de PI con delegados

delegate void PiDelegate (int precision);

private void buttonCalc_Click(object sender,


System.EventArgs e)
{
PiDelegate delegado = new PiDelegate(CalcularPi);

delegado.BeginInvoke((int)editDigits.Value, null, null);


}

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.

Si nos preocupase que la hebra que calcula el valor de PI quedase bloqueada


(como antes la hebra de la interfaz gráfica), tendríamos que utilizar los métodos
asíncronos BeginInvoke y EndInvoke. En el ejemplo que nos ocupa nos basta
con el método síncrono Invoke:
public object Invoke(Delegate method);
public object Invoke(Delegate method, object[] args);

La segunda variante del método es la que utilizaremos en nuestra hebra para


llamar a ShowProgress especificando sus parámetros.

 Creamos un delegado para la función que deseamos llamar:


 delegate void ShowProgressDelegate
 (string pi, int actual, int total);
 Sustituimos las llamadas a ShowProgress por:
 ShowProgressDelegate showProgress =
 new ShowProgressDelegate(ShowProgress);
 ...
 this.Invoke(showProgress, new object[] { pi.ToString(),
calculados, precision});

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:

En el ejemplo anterior, si implementamos la función ShowProgress de la siguiente


forma, podemos llamar a la función de la forma tradicional sin preocuparnos de la
hebra desde la que utilizamos nuestra función:
void ShowProgress(string pi, int actual, int total)
{
if ( textPI.InvokeRequired == false ) {
// Hebra correcta
textPI.Text = pi;
progress.Maximum = total;
progress.Value = actual;
} else {
// Llamar a la función de forma asíncrona
ShowProgressDelegate showProgress = new
ShowProgressDelegate(ShowProgress);
this.Invoke(showProgress, new object[] { pi, actual, total});
}
}
Si tenemos en cuenta que la función ShowProgress no devuelve ningún valor (esto
es, en realidad se trata de un procedimiento), podemos sustituir la llamada síncrona
Invoke por una llamada asíncrona con BeginInvoke:
BeginInvoke(showProgress, new object[] { pi, actual, total});
BeginInvoke es siempre preferible cuando la función no devuelve ningún valor ya
que evita que se puedan producir bloqueos.
Hebras en aplicaciones web
Las peculiaridades de las interfaces web ocasionan la aparición de problemas de los cuales no
tendríamos que preocuparnos en otros contextos. Éste es el caso cuando nuestra aplicación
web debe realizar una tarea relativamente larga. Lo que está claro es que no queda demasiado
bien de cara al usuario dejar su ventana en blanco de forma indefinida mientras nuestra
aplicación realiza los cálculos que sean necesarios.

Una solución a este problema involucra la utilización de hebras. El problema de realizar un


cálculo largo lo volvemos a descomponer en dos hebras:

 La hebra principal se encargará de mostrarle al usuario el estado actual de la


aplicación, estado que se refrescará en su navegador automáticamente
gracias al uso de la cabecera Refresh, definida en el estándar para las
respuestas HTTP.
 Una hebra auxiliar será la encargada de ejecutar el código correspondiente a
efectuar todos los cálculos que sean necesarios para satisfacer la solicitud
del usuario.

Hebras en aplicaciones web


Una página ASP.NET lanza la tarea:

...
using System.Threading;

public class Payment : System.Web.UI.Page


{
protected Guid ID; // Identificador de la solicitud

private void Page_Load(object sender, System.EventArgs e)


{
if (Page.IsPostBack) {

// 1. Crear un ID para la solicitud

ID = Guid.NewGuid();

// 2. Lanzar la hebra

ThreadStart ts = new ThreadStart(RealizarTarea);


Thread thread = new Thread(ts);
thread.Start();

// 3. Redirigir a la página de resultados

Response.Redirect("Result.aspx?ID=" + ID.ToString());
}
}

private void RealizarTarea ()


{
// Tarea larga !!!
System.Threading.Thread.Sleep(new TimeSpan(0, 0, 0, 7, 0));
object resultado = new Random(0).Next(100);

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;

public sealed class Results


{
private static Hashtable results = new Hashtable();

public static object Get(Guid ID)


{
return results[ID];
}

public static void Add (Guid ID, object result)


{
results[ID] = result;
}

public static void Remove(Guid ID)


{
results.Remove(ID);
}

public static bool Contains(Guid ID)


{
return results.Contains(ID);
}
}
Finalmente, la página encargada de mostrar los resultados se refresca
automáticamente hasta que la ejecución de la hebra auxiliar haya terminado y sus
resultados estén disponibles:
public class Result : System.Web.UI.Page
{
protected System.Web.UI.WebControls.Label lblMessage;

private void Page_Load(object sender, System.EventArgs e)


{
Guid ID = new Guid(Page.Request.QueryString["ID"]);

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).

 Comenzamos definiendo una variable de estado:


 enum Estado { Inactivo, Calculando, Cancelando };

 Estado estado = Estado.Inactivo;

 Implementamos la respuesta de nuestro botón en función del estado actual de la


operación:
 private void buttonCalc_Click(object sender, System.EventArgs e)
 {
 switch (estado) {

 case Estado.Inactivo: // Comenzar el cálculo
 estado = Estado.Calculando;
 buttonCalc.Text = "Cancelar";
 PiDelegate delegado = new PiDelegate(CalcularPi);
 delegado.BeginInvoke((int)editDigits.Value, null, null);
 break;

 case Estado.Calculando: // Cancelar operación
 estado = Estado.Cancelando;
 buttonCalc.Enabled = false;
 break;

 case Estado.Cancelando: // No debería suceder
nunca...
 Debug.Assert(false);
 break;
 }
 }

 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;
 }

¡OJO! Cuando varias hebras acceden simultáneamente a algún recurso compartido es


necesario implementar algún tipo de mecanismo de exclusión mutua: en el desarrollo
de aplicaciones multihebra siempre debemos garantizar la exclusión mutua en el acceso
a recursos compartidos. En el caso anterior, la aplicación funciona porque sólo una de
las hebras modifica el valor de la variable compartida y, además, lo restablece sólo
después de que la otra hebra haya comprobado el valor de la variable compartida y
haya decidido finalizar su ejecución (EndProgress). En general, no obstante,
deberíamos estudiar las posibles condiciones de carrera y la posibilidad de bloqueos si
empleamos técnicas de exclusión mutua...
Código fuente del ejemplo (Visual Studio .NET 2003)
Código fuente del ejemplo (Visual Studio .NET 2005)
Paralelismo real
Podemos aprovechar el paralelismo real de nuestra máquina para reducir el tiempo de reloj
necesario para realizar un cálculo costoso si disponemos de un multiprocesador, de un
microprocesador de doble (o cuádruple) núcleo o simplemente tenemos un procesador SMT
(Simultaneous Multithreading, lo que Intel hace comercializa bajo el nombre HyperThreading).

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

enum Estado { Inactivo, Calculando, Cancelando };

Estado estado = Estado.Inactivo;

// Evento asociado al ratón

private void buttonCalc_Click(object sender, System.EventArgs e)


{
switch (estado) {

case Estado.Inactivo: // Comenzar el


cálculo
estado = Estado.Calculando; // con varias hebras
buttonCalc.Text = "Cancelar";
textPI.Text = "";
setPrecision((int)editDigits.Value);
startThread();
startThread();
break;

case Estado.Calculando: // Cancelar operación


estado = Estado.Cancelando;
buttonCalc.Enabled = false;
break;
}
}

La actualización de la interfaz de usuario la hacemos de forma similar a como la


hacíamos antes, si bien hemos simplificado algo la signatura de los métodos
utilizados ya que, ahora, la precisión deseada de pi será una variable de instancia de
nuestra clase:

delegate void ShowProgressDelegate(int actual);


delegate void EndProgressDelegate();

private void ShowProgress (int completed)


{
Debug.Assert(textPI.InvokeRequired == false);

progress.Maximum = precision;
progress.Value = Math.Min(precision, 9*completed);
}

private void EndProgress ()


{
if (estado != Estado.Cancelando)
textPI.Text = Pi();
else
textPI.Text = "Operación cancelada";

estado = Estado.Inactivo;
buttonCalc.Text = "Calcular";
buttonCalc.Enabled = true;
}

Una vez más, utilizaremos un delegado para nuestras hebras:

delegate void PiDelegate();

A continuación, implementamos las operaciones que nos permitirán lanzar la


ejecución de las hebras (startThread) y terminar su ejecución de manera
ordenada (endThread). Usaremos, de forma segura, un contador auxiliar ( hebras)
para que sólo se actualice la interfaz de usuario cuando termine la ejecución de la
última hebra:

int hebras = 0;

private void startThread()


{
PiDelegate delegado = new PiDelegate(CalcularPi);

delegado.BeginInvoke(null, null);

lock (this) {
hebras++;
}
}

private void endThread ()


{
lock (this) {
hebras--;
}

if (hebras==0) {
EndProgressDelegate endProgress = new
EndProgressDelegate(EndProgress);
this.Invoke(endProgress);
}
}

El cálculo del valor exacto de pi lo iremos haciendo por segmentos independientes y


sólo reconstruiremos su valor al terminar (para evitar posibles cuellos de botella que
se producirían si continuamente tuviésemos que estar sincronizando la ejecución de
las distintas hebras):

private int precision;


private int[] segments;
private int current;

private void setPrecision (int precision)


{
this.precision = precision;
this.current = 0;
this.segments = new int[precision/9+1];
}

A partir de esos "segmentos" (conjuntos de dígitos que estarán incluidos en la parte


decimal del número pi), la reconstrucción de pi será inmediata:

private string Pi ()
{
StringBuilder pi = new StringBuilder("3.", precision + 2);

for (int i=0; i<segments.Length; i++) {


string ds = string.Format("{0:D9}", segments[i]);
int nuevos = Math.Min(precision - 9*i, 9);
pi.Append(ds.Substring(0, nuevos));
}

return pi.ToString();
}

Para ir repartiendo el trabajo entre las distintas hebras, definimos un método


auxiliar que le indica a cada hebra qué segmento del número pi ha de calcular a
continuación:

private int nextSegment()


{
int segment = current;

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:

private void CalcularPi ()


{
int segment;

ShowProgressDelegate showProgress = new


ShowProgressDelegate(ShowProgress);
this.Invoke(showProgress, new object[] { 0 });
segment = nextSegment();

while ((estado!=Estado.Cancelando) && (segment<segments.Length)) {

segments[segment] = NineDigitsOfPi.StartingAt(9*segment+1);

this.Invoke(showProgress, new object[] { segment });


segment = nextSegment();
}

endThread();
}

Con estas modificaciones, podemos reducir prácticamente un 50% el tiempo de


ejecución necesario para obtener el valor de PI con la precisión deseada si
disponemos de un procesador de doble núcleo o de un multiprocesador con dos
procesadores (el ahorro sería mayor incluso si disponemos de mayor paralelismo en
el hardware y lanzamos más hebras en paralelo).
Código fuente de la aplicación multihebra para procesadores SMT y multiprocesadores

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.

Uso de recursos compartidos


Cuando varias hebras comparten el uso de un recurso, pueden ocurrir situaciones no deseadas
si dos o más hebras acceden (o intentan acceder) al mismo recurso simultáneamente.

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.

En definitiva, las aplicaciones multihebra deben evitar el acceso concurrente a recursos


compartidos (p.ej. datos comunes), para lo cual pueden utilizar distintos mecanismos de
exclusión mutua:

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

// Entrada en la sección crítica

Monitor.Enter(this);

try {

// Acciones que requiran un acceso coordinado

...

} finally {

// Salida de la sección crítica

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) {

// Acciones que requiran un acceso coordinado


// ...
}

El acceso a objetos compartidos cuyo estado puede variar ha de hacerse siempre de forma
protegida:

public class Account


{
decimal balance;

public void Deposit(decimal amount)


{
lock (this) {
balance += amount;
}
}

public void Withdraw(decimal amount)


{
lock (this) {
balance -= amount;
}
}
}
Cajero automático
Crear un simulador de cajero automático introduciendo llamadas a Thread.Sleep
para provocar el entrelazado de distintas operaciones sobre una cuenta.
Comprobar las diferencias que existen cuando usamos y cuando no usamos lock.

Cajero automático defectuoso: Código fuente para Visual Studio 2003


Cajero automático defectuoso: Código fuente para Visual Studio 2005

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:

public class Cache


{
public static void Add(object x) {
lock (typeof(Cache)) {
...
}
}
public static void Remove(object x) {
lock (typeof(Cache)) {
...
}
}
}

El uso de mecanismos de sincronización como cerrojos o monitores puede causar problemas de


inanición (cuando un proceso queda bloqueado indefinidamente) e interbloqueos (p.ej. cuando
el orden de adquisición de los cerrojos no siempre es el mismo).

Lectores y escritores...

class RWLock
{
int lectores = 0;

public void AcquireExclusive()


{
lock (this) {
while (lectores!=0) Monitor.Wait(this);
lectores--;
}
}

public void AcquireShared()


{
lock (this) {
while (lectores<0) Monitor.Wait(this);
lectores++;
}
}

public void ReleaseExclusive()


{
lock (this) {
lectores = 0;
Monitor.PulseAll(this);
}
}

public void ReleaseShared()


{
lock (this) {
lectores--;
if (lectores==0) Monitor.Pulse(this);
}
}
}

Los mecanismos de sincronización anteriores utilizan memoria compartida y son útiles en


sistemas centralizados. En un sistema distribuido, la comunicación y sincronización entre
procesos se habrá de realizar mediante paso de mensajes, ya sea mediante denominación
directa (haciendo referencia al proceso con el que deseamos comunicarnos o canal a través del
cual realizaremos la comunicación) o a través de nombres globales (buzones y puertos).
Otros mecanismos de sincronización
En el espacio de nombres System.Threading:

 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;

callback = new AsyncCallback(CallbackFunction);


state = new StateObject();

fs = new FileStream ( "fichero.txt",


FileMode.Open,
FileAccess.Read,
FileShare.Read, 1, true);

state.fs = fs;
state.size = fs.Length;
state.bytes = new byte[state.size];

fs.BeginRead ( state.bytes, 0, state.size,


callback, state);
...
3. Procesamiento de los datos leídos (de forma independiente):
public static void CallbackFunction
(IAsyncResult asyncResult)
{
StateObject state;
Stream stream;

state = (StateObject) asyncResult.AsyncState;


stream = state.fs;

int bytesRead = stream.EndRead(asyncResult);

if (bytesRead != state.size)
throw new Exception
("Sólo se han leído {0} bytes", bytesRead);

stream.Close();

// Operaciones con los datos (state.bytes)


...

// Notificación de la finalización de la operación


Monitor.Enter(WaitObject);
Monitor.Pulse(WaitObject);
Monitor.Exit(WaitObject);
}
4. Espera a la finalización de la operación de lectura (desde la hebra principal):
Monitor.Enter(WaitObject);
Monitor.Wait(WaitObject);
Monitor.Exit(WaitObject);

Información adicional

Véanse los métodos BeginRead/EndRead y BeginWrite/EndWrite de la clase


System.IO.Stream.
Distribución

Comunicación entre procesos

Cuando en un sistema tenemos distintos procesos independientes ejecutándose, necesitamos


disponer de mecanismos que hagan posible la comunicación entre ellos. Se pueden utilizar
distintos mecanismos de comunicación entre procesos [IPC: InterProcess Communication]:

 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.

Como se puede ver, disponemos de una amplia variedad de mecanismos de comunicación


entre procesos. Algunos de ellos facilitan la división del trabajo entre distintos procesos de una
máquina, otros permiten dividir el trabajo entre procesos que se ejecutan en distintos
ordenadores de un sistema distribuido.

Independientemente del mecanismo de comunicación entre procesos que decidamos emplear,


nuestra aplicación debería acceder a los recursos externos de la misma forma que accede a
recursos locales. Para encapsular el acceso a recursos externos se suelen emplear proxies o
gateways:

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.

NOTA: Usualmente, la aplicaciones que utilizan mecanismos de comunicación entre procesos


suelen clasificarse como clientes o servidores, si bien pueden desempeñar ambos roles en
distintos momentos. El cliente es el que solicita acceder a algún servicio proporcionado por
otro proceso. El servidor es el que atiende las peticiones de los clientes.
Sockets

La familia de protocolos TCP/IP


La familia de protocolos TCP/IP [Transmission Control Protocol / Internet Protocol] define una
serie de estándares que proporcionan la base necesaria para el funcionamiento de Internet:

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

La capa de red en el modelo OSI es la encargada de proporcionar servicios a la capa de


transporte y es responsable del enrutamiento de los paquetes desde su origen hasta su
destino. En el caso del protocolo IPv4, se utilizan direcciones de 32 bits para hacer referencia a
los extremos de la comunicación:

Algunas direcciones IP están reservadas para propósitos específicos:

Los protocolos TCP y UDP: La capa de transporte en Internet

La capa de transporte se encarga de proporcionar servicios a las distintas aplicaciones que


hacen uso de la red. En el caso de la familia TCP/IP, el protocolo TCP [Transmission Control
Protocol, RFC 793] proporciona servicios orientados a conexión, mientras que el protocolo UDP
[User Datagram Protocol, RFC 768] proporciona servicios no orientados a conexión.

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:

Puerto TCP Protocolo Uso


21 FTP Transferencia de ficheros
23 Telnet Acceso remoto
25 SMTP Envío de correo electrónico
79 Finger Información acerca de usuarios
80 HTTP World Wide Web
110 POP3 Recepción de correo electrónico
119 NNTP Grupos de noticial USENET
143 IMAP Lectura de correo electrónico
... ... ...

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.

Creación de un servidor TCP

La creación de un servidor TCP no resulta demasiado complicada. En primer lugar, debemos


establecer el socket a través del cual el servidor aceptará peticiones, para lo cual nos hace
falta una dirección IP (la correspondiente a la máquina en la que se ejecute el servidor) y un
número de puerto TCP:

// DNS: nombre del host -> dirección IP


IPHostEntry ipHostInfo = Dns.Resolve(Dns.GetHostName());
IPAddress ipAddress = ipHostInfo.AddressList[0];

// 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:

Socket listener = new Socket (


AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp );
listener.Bind(localEndPoint);
listener.Listen(100);

El método Listen pone el socket en estado de escucha y su parámetro establece la longitud


máxima de la cola de conexiones pendientes que puede tener el servidor antes de empezar a
rechazar conexiones [backlog, en inglés].
A continuación, nos quedamos esperando a que un cliente establezca una conexión con
nuestro servidor. El método Accept extrae la primera petición de la cola asociada al servidor y
devuelve un nuevo socket que podemos utilizar para comunicarnos con el cliente:

Socket handler = listener.Accept();

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:

byte[] bytes = new byte[1024];


int count;
String data = "";

do
{
count = handler.Receive(bytes);
data += System.Text.Encoding.ASCII.GetString(bytes,0,count);

} while ( data.IndexOf("\n") == -1 );

// Eco

Console.WriteLine( "Texto recibido: {0}", data);

byte[] msg = System.Text.Encoding.ASCII.GetBytes(data);

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:

telnet localhost 11000

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:

TcpListener listener = new TcpListener(11000);

listener.Start();

TcpClient client = listener.AcceptTcpClient();

...
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)

Implementación de un cliente TCP

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:

string mensaje = "Hola...\n";


string respuesta;
Byte[] SendBytes = Encoding.ASCII.GetBytes(mensaje);
Byte[] RecvBytes = new Byte[256];
int bytes;

// 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);

bytes = socket.Receive ( RecvBytes,


RecvBytes.Length,
SocketFlags.None);

respuesta = Encoding.ASCII.GetString
(RecvBytes, 0, bytes);

while (bytes > 0) {

bytes = socket.Receive ( RecvBytes,


RecvBytes.Length,
SocketFlags.None);

respuesta += Encoding.ASCII.GetString
(RecvBytes, 0, bytes);
}

Console.WriteLine(respuesta);

} catch (Exception error) {


Console.WriteLine("ERROR - "+error);
}
Código fuente del cliente TCP utilizando sockets (Visual Studio 2003)
Código fuente del cliente TCP utilizando sockets (Visual Studio 2005)

Igual que antes, la plataforma .NET incluye otra clase auxiliar que nos puede servir de utilidad
para crear clientes TCP: la clase TcpClient.

string mensaje = "Hola... \n";


string respuesta;
Byte[] SendBytes = Encoding.ASCII.GetBytes(mensaje);
Byte[] RecvBytes = new Byte[256];
int bytes;

// Cliente TCP

TcpClient client = new TcpClient();


NetworkStream stream;

// Conexión

try {

client.Connect("localhost",11000);

stream = client.GetStream();

stream.Write ( SendBytes,
0,SendBytes.Length );

bytes = stream.Read ( RecvBytes,


0, RecvBytes.Length);

respuesta = Encoding.ASCII.GetString
(RecvBytes, 0, bytes);

while (bytes > 0) {

bytes = stream.Read ( RecvBytes,


0, RecvBytes.Length );

respuesta += Encoding.ASCII.GetString
(RecvBytes, 0, bytes);
}

Console.WriteLine(respuesta);

} catch (Exception error) {


Console.WriteLine("ERROR - "+error);
}
Código fuente del cliente TCP utilizando TcpClient (Visual Studio 2003)
Código fuente del cliente TCP utilizando TcpClient (Visual Studio 2005)

Desarrollo de un servidor TCP real

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 );

// Finalización de la ejecución del servidor


Console.Read();
}
...

Con los métodos Begin... realizaremos las operaciones de E/S de nuestro


servidor de forma asíncrona. Para ello, tendremos que definir una clase auxiliar
que se encargue de mantener el estado de cada operación asíncrona de E/S:

public class StateObject


{
// Tamaño del buffer
public const int BufferSize = 1024;

// Socket.
public Socket workSocket = null;

// Buffer de recepción de datos


public byte[] buffer = new byte[BufferSize];

// 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):

public static void AcceptCallback (IAsyncResult ar)


{
Socket listener = (Socket)ar.AsyncState;

// 1. Nuevo socket para aceptar otras conexiones


concurrentemente

listener.BeginAccept(new AsyncCallback(AcceptCallback),
listener);

// 2. Aceptar conexión recibida

Socket handler = listener.EndAccept(ar);

// 3. Recibir datos de forma asíncrona

StateObject state = new StateObject();


state.workSocket = handler;

handler.BeginReceive ( state.buffer, 0, StateObject.BufferSize,


0,
new AsyncCallback(ReadCallback), state);
}

La recepción de datos se realiza de forma similar a los ejemplos anteriores, si


bien cada operación de E/S se realizará de forma asíncrona (de ahí lo de volver a
invocar a BeginReceive si aún no han llegado todos los datos del cliente):

public static void ReadCallback(IAsyncResult ar)


{
String content = String.Empty;
StateObject state = (StateObject)ar.AsyncState;
Socket handler = state.workSocket;

int bytesRead = handler.EndReceive(ar);

if (bytesRead > 0) {

// Almacenar los datos recibidos

state.sb.Append(Encoding.ASCII.GetString(state.buffer, 0,
bytesRead));

content = state.sb.ToString();

if (content.IndexOf("\n") > -1) {


// Enviar eco
Send(handler, content);
} else {
// Recibir más datos...
handler.BeginReceive(state.buffer, 0,
StateObject.BufferSize, 0,
new AsyncCallback(ReadCallback),
state);
}
}
}

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.BeginSend( byteData, 0, byteData.Length, 0,


new AsyncCallback(SendCallback), handler);
}

private static void SendCallback(IAsyncResult ar)


{
try {

Socket handler = (Socket)ar.AsyncState;

handler.EndSend(ar);
handler.Shutdown(SocketShutdown.Both);
handler.Close();

} catch (Exception e) {
Console.WriteLine(e.ToString());
}
}

Código fuente del servidor TCP multihebra (Visual Studio 2005)


Acceso a recursos en la web
Aparte de proporcionar los mecanismos básicos de comunicación entre procesos mediante
sockets, la biblioteca de clases de la plataforma .NET incluye clases auxiliares que nos facilitan
el uso de los protocolos asociados a distintas aplicaciones comunes. En el caso de la World
Wide Web, la aplicación más popular de Internet, podemos utilizar las clases WebClient,
HttpWebRequest y HttpWebResponse. Estas clases nos permiten acceder a cualquier
recurso disponible a través de HTTP o de HTTPS sin necesidad de implementar ni conocer los
detalles de estos protocolos.

string url = "http://...";


HttpWebRequest request;
HttpWebResponse response;

request = (HttpWebRequest) WebRequest.Create(url);

request.KeepAlive = false;

response = (HttpWebResponse) request.GetResponse();

// Información proporcionada por el protocolo HTTP

Console.WriteLine ("Protocol = "


+ response.ProtocolVersion.ToString());

Console.WriteLine ("Status = "


+ response.StatusCode);

Console.WriteLine ("Description = "


+ response.StatusDescription.ToString());

Console.WriteLine ("Character Set = "


+ response.CharacterSet);

Console.WriteLine ("Encoding = "


+ response.ContentEncoding);

Console.WriteLine ("Content = "


+ response.ContentType);

Console.WriteLine ("Method = "


+ response.Method);

Console.WriteLine ("URI = " + response.ResponseUri);

Console.WriteLine ("Server = " + response.Server);

Console.WriteLine ("Cookies = " + response.Cookies);

Console.WriteLine ("Last modified = "


+ response.LastModified);

// Cabecera de la respuesta HTTP

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 = new StreamReader( stream, encoder );

Char[] read = new Char[256];

int count = reader.Read( read, 0, 256 );

while (count > 0) {


String str = new String(read, 0, count);
Console.Write(str);
count = reader.Read(read, 0, 256);
}

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).

Envío de correo electrónico en Visual Studio 2005 (.NET Framework


2.0)

Hemos de utilizar las clases definidas en el espacio de nombres System.Net.Mail. Dentro de


este espacio de nombres, la clase System.Net.Mail.SmtpClient es la que nos permite
acceder a un servidor SMTP para enviar mensajes de correo electrónico:

SmtpClient smtp = new SmtpClient("smtp.gmail.com");

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:

SmtpClient smtp = new SmtpClient("smtp.gmail.com");

smtp.Credentials = new
System.Net.NetworkCredential("usuario","********");
smtp.EnableSsl = true;
smtp.Send(from,to,subject,body);

La clase System.Net.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:

MailMessage message = new MailMessage("[email protected]",


destinatario);

message.Subject = "...";
message.IsBodyHtml = true;
message.Body = "<html><body> ... </body></html>";

SmtpClient smtp = new SmtpClient("smtp.gmail.com");

smtp.Credentials = new System.Net.NetworkCredential("usuario",


"********");
smtp.EnableSsl = true;

smtp.Send(message);

Incluso podemos adjuntar ficheros a nuestros mensajes de correo electrónico empleando la


clase System.Net.Mail.Attachment:

OpenFileDialog dialog = new OpenFileDialog();

dialog.InitialDirectory = "c:\\" ;
dialog.Filter = "Ficheros de texto (*.txt)|*.txt" +
+ "|Cualquier fichero (*.*)|*.*" ;
dialog.FilterIndex = 2 ;
dialog.RestoreDirectory = true;

if (dialog.ShowDialog() == DialogResult.OK) {

MailMessage message = new MailMessage("[email protected]",


destinatario);
Attachment attachment = new Attachment(dialog.FileName);

message.Subject = "...";
message.IsBodyHtml = true;
message.Body = "<html><body>"+...+"</body></html>";
message.Attachments.Add(attachment);

SmtpClient smtp = new SmtpClient("smtp.gmail.com");

smtp.Credentials = new System.Net.NetworkCredential("usuario",


"********");
smtp.EnableSsl = true;

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)

Hemos de utilizar las clases definidas en el espacio de nombres System.Web.Mail. Dentro de


este espacio de nombres, la clase SmtpMail es la que sirve para enviar mensajes de correo
electrónico:
System.Web.Mail.SmtpMail.Send(from, to, subject, body);
Por defecto, esta clase intenta utilizar un servidor SMTP que se encuentre instalado en la
máquina local (si bien podemos utilizar otro servidor SMTP estableciendo la propiedad estática
SmtpServer de la clase SmtpMail). Si nos decantamos por utilizar un servidor SMTP local,
podemos lanzar el servidor virtual SMTP que viene con el Internet Information Server. Para
activarlo, basta con seleccionar la opción "Iniciar" del menú contextual asociado al servidor
virtual SMTP en la utilidad de administración de equipos de Windows XP:

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;

message = new System.Web.Mail.MailMessage();


message.From = ...;
message.To = ...;
message.Subject = ...;
message.BodyFormat = System.Web.Mail.MailFormat.Html;
message.Body = "<html><body> ... </body></html>";

System.Web.Mail.SmtpMail.Send(message);

Incluso podemos adjuntar ficheros a nuestros mensajes de correo electrónico empleando la


clase System.Web.Mail.MailAttachment:

OpenFileDialog dialog = new OpenFileDialog();

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 = new System.Web.Mail.MailMessage();


message.From = ...;
message.To = ...;
message.Subject = ...;
message.BodyFormat = System.Web.Mail.MailFormat.Html;
message.Body = "<html><body>"+...+"</body></html>";

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)

Si queremos utilizar un servidor de correo que requiera autentificación, debemos escribir lo


siguiente para poder enviar mensajes de correo electrónico:

using System.Web.Mail
...

MailMessage message;

message.From = "[email protected]";
message.Bcc = "[email protected]";

message.Headers.Add ( "Reply-To",
"[email protected]");
message.Headers.Add ( "From",
"Fernando Berzal <[email protected]>");

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):

Thread listener; // Hebra empleada


// para recibir mensajes
La ejecución de esta hebra auxiliar para recibir mensajes la controlaremos desde los eventos
Form_Load y Form_Closed del formulario (el flag done lo utilizaremos más adelante para
asegurarnos de que la hebra auxiliar termina su ejecución):

bool done = false; // Flag empleado para terminar


// la ejecución de la hebra

// Arranque

private void ChatForm_Load


(object sender, System.EventArgs e)
{
ThreadStart start = new ThreadStart(Listener);
listener = new Thread(start);
listener.Start();
}

// Detención

private void ChatForm_Closed


(object sender, System.EventArgs e)
{
done = true;
Send("FIN");
listener.Join();
}

La estructura general de una que recibe mensajes siempre es la misma:

private void Listener()


{
while (!done) {
...
}
}

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:

// Actualización del texto (thread-safe)

delegate void AppendTextDelegate (string text);

void AppendText (string text)


{
if (textBoxDialog.InvokeRequired == false) {

// Hebra correcta
textBoxDialog.AppendText(text);

} else {

// Llamar al método de forma asíncrona


AppendTextDelegate appendText = new
AppendTextDelegate(AppendText);
this.Invoke(appendText, new object[] { text });
}
}

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:

private void Send (string message)


{
IPEndPoint ep;
Socket s;
byte[] msg;

ep = new IPEndPoint broadcastAddress, Port);

s = new Socket ( broadcastAddress.AddressFamily,


SocketType.Dgram,
ProtocolType.Udp);

msg = Encoding.Default.GetBytes( message );

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:

private void textBoxUser_KeyPress


( object sender,
System.Windows.Forms.KeyPressEventArgs e)
{
if (e.KeyChar == (char)13) {
Send ( textBoxUser.Text );
textBoxUser.ResetText();
}
}

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:

private void Listener()


{
IPEndPoint localEP;
Socket socket;
localEP = new IPEndPoint(localAddress, Port);

socket = new Socket ( localAddress.AddressFamily,


SocketType.Dgram,
ProtocolType.Udp );

try {

socket.Bind(localEP);

while (!done) {

// Buffer para recibir el mensaje


byte[] buffer = new byte[1024];

// IPEndPoint para identificar al emisor


IPEndPoint sender = new IPEndPoint (IPAddress.Any, 0);
EndPoint remoteEP = (EndPoint) sender;

// Recepción del mensaje (ojo: bloquea al receptor)

socket.ReceiveFrom (buffer, ref remoteEP);

// Actualización segura de la interfaz de usuario

string mensaje = Encoding.Default.GetString(buffer);

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 );

unsigned long AddRef();

unsigned long Release();


}

La forma de hacer referencia a un interfaz COM es a través de su identificador IID [Interface


IDentifier], un número de 128 bits único a nivel global [GUID: Globally Unique IDentifier].

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/DCOM se utiliza mediante la generación automática de proxies en el cliente y stubs en el


servidor que se encargan de encapsular la comunicación entre procesos. De cara al
programador, ésta se realiza de forma transparente. Los proxies y los stubs se generan
automáticamente a partir de la especificación de los interfaces en MIDL [Microsoft COM
Interface Definition Language], un ejemplo de la cual aparece arriba en la definición de
IUnknown.

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 CLR se encarga de permitir la realización de llamadas que atraviesen los límites de un


dominio, de un proceso y de una máquina. Cuando los objetos están en el mismo dominio, la
llamada es local. En cualquier otro caso, la llamada es remota (de ahí lo de .NET Remoting),
incluso cuando los objetos estén en el mismo proceso pero en diferente dominio. Las llamadas
locales son inmediatas (como en cualquier invocación a una función en un lenguaje de
programación tradicional), mientras que las llamadas remotas involucran el uso de
"marshalling" a través de proxies.

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;
...

AppDomain domain = AppDomain.CurrentDomain;

Console.WriteLine("Dominio actual: " +


domain.FriendlyName);

Assembly[] loadedAssemblies = domain.GetAssemblies();

Console.WriteLine("Assemblies en el dominio actual:");

foreach (Assembly assembly in loadedAssemblies)


Console.WriteLine (assembly.FullName);
Código fuente de ejemplo

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

Los canales proporcionan el medio mediante el cual transmitir mensajes de extremo a


extremo. Se encargan de transmitir mensajes entre dominios de aplicación, puede que en
distintas máquinas conectadas a través de una red de ordenadores, entre distintos procesos
concurrentes que se ejecutan en una máquina o, simplemente, entre dominios de aplicación
dentro de un proceso. La plataforma .NET lleva incorporadas implementaciones de los canales
para realizar la comunicación mediante sockets sobre TCP o HTTP.

Un canal normal, HTTP o TCP, no incorpora ninguna medida de seguridad a la hora de


transmitir datos. Si queremos que la transmisión de datos se realice de forma segura,
tendremos que crear nuestro propio canal (creando una clase que implemente los interfaces
IChannelReceiver, IChannel e IChannelSender) o alojar nuestros objetos en el IIS, al cual se
puede acceder usando HTTPS si lo configuramos adecuadamente.

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

"Marshalling" es el mecanismo mediante el cual se empaquetan las llamadas entre dominios de


aplicación para su transmisión (tanto el paso de parámetros como la devolución de
resultados). Se puede acceder de forma remota a un objeto de dos formas diferentes:

 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

Un "proxy" [apoderado] es un objeto que actúa localmente en nombre de un objeto remoto.


Desde el punto de vista del programador, el proxy acepta llamadas como si fuese el objeto
real, si bien internamente lo único que hace es delegar en el objeto remoto para ejecutar las
llamadas que recibe.

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:

 Si al otro extremo no se está escuchando, no se puede realizar ninguna conexión (en


COM, la activación se produce bajo demanda).
 No existe ningún registro a modo de páginas amarillas (como sucede en RMI, por
ejemplo).
 Los servidores no se activan remotamente (en este sentido, .NET Remoting se parece
más a los sockets TCP que a los componentes 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.

Uso de dominios de aplicación dentro de un proceso

Comencemos creando un servidor ultra-simple (servidor.exe):

using System;

namespace RemotingExample
{
public class Servidor
{
[STAThread]
static void Main(string[] args)
{
Console.WriteLine ("Servidor cargado y ejecutado");
}

// Dominio en el que se ejecuta el servidor

public string Domain


{
get { return AppDomain.CurrentDomain.ToString(); }
}
}
}

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).

Ya en el cliente, creamos un nuevo dominio de aplicación (dentro del mismo proceso) y


cargamos en él el assembly servidor.exe:

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");

s = o.Unwrap(); // ... para forzar la instanciación del objeto

Console.WriteLine(s + " @ " + s.Domain);

// Fin

Console.WriteLine("Pulse ENTER...");
Console.ReadLine();
}
}
}

CreateInstance recibe como parámetros el assembly en el que se encuentra nuestro


"servidor" y el nombre completo de la clase a la que pertenece nuestro servidor.

Código fuente del ejemplo: Servidor no serializable (Visual Studio 2003)


Código fuente del ejemplo: Servidor no serializable (Visual Studio 2005)

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");
}

// Dominio en el que se ejecuta el servidor

public string Domain


{
get { return AppDomain.CurrentDomain.ToString(); }
}
}
}
Código fuente del ejemplo: Paso por valor (Visual Studio 2003)
Código fuente del ejemplo: Paso por valor (Visual Studio 2005)

También podríamos controlar explícitamente cómo se serializa un objeto si, en vez de


limitarnos a utilizar el atributo [Serializable], implementásemos explícitamente la interfaz
ISerializable.

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");
}

// Dominio en el que se ejecuta el servidor

public string Domain


{
get { return AppDomain.CurrentDomain.ToString(); }
}
}
}
Código fuente del ejemplo: Paso por referencia (Visual Studio 2003)
Código fuente del ejemplo: Paso por referencia (Visual Studio 2005)

Acceso a objetos remotos con .NET Remoting

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:

public class Servidor: System.MarshalByRefObject


{
public Servidor()
{
Console.WriteLine("Constructor");
}
~Servidor()
{
Console.WriteLine("Destructor");
}

public string GetInfo ()


{
return AppDomain.CurrentDomain.FriendlyName;
}
}

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());

Servidor remoto = (Servidor)Activator.GetObject (


typeof(RemotingExample.Servidor),
"http://localhost:8888/Servidor" );

Console.WriteLine( remoto.GetInfo() );

// Acceso local

Servidor local = new Servidor();

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.

Código fuente del ejemplo (Visual Studio 2003)


Código fuente del ejemplo (Visual Studio 2005)

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

Cuando el cliente retoma el control...

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")};

ObjectHandle handle = Activator.CreateInstance


( "Servidor", "RemotingExample.Servidor", attrs);

Servidor remoto = (Servidor) handle.Unwrap();

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".

Un objeto remoto puede definir su ciclo de vida redefiniendo el método


InitializeLifeTimeService() de la clase base MarshalByRefObject. Cuando el objeto
se crea, la duración de su contrato de alquiler se fija usando la propiedad InitialLeaseTime
del contrato representado por un objeto que implementa la interfaz ILease. La duración
predeterminada del contrato es de 300 segundos por defecto, aunque eso se puede modificar
en la aplicación que aloja el objeto remoto (de forma global para todos los objetos de la
aplicación) o en el propio objeto remoto (de forma local a cada objeto):

// En la aplicación del servidor

public class Host


{
static void Main(string[] args)
{
LifetimeServices.LeaseTime = TimeSpan.FromMinutes(1);
LifetimeServices.RenewOnCallTime = TimeSpan.FromSeconds(30);
...
}
...
}

// En el objeto remoto

public class ObjetoRemoto : MarshalByRefObject


{
...
public override Object InitializeLifetimeService()
{
ILease lease = (ILease)base.InitializeLifetimeService();

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.

Afortunadamente, esto no es necesario porque se pueden usar ficheros de configuración XML


de forma que en el código no tengamos que saber de qué tipo concreto son los objetos a los
que se accede (aunque siempre deberemos ser conscientes de si el objeto remoto es un SAO
SingleCall, un SAO Singleton o un CAO).

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.

Como es lógico, en el cliente también podemos usar un fichero de configuración análogo y no


tener que especificar los datos relativos a la configuración del sistema (a costa de que un
usuario malintencionado pueda acceder fácilmente a esos datos, disponibles en XML). Además,
en el caso del cliente, ya no tenemos que usar Activator para instanciar un objeto remoto.
Bastará con crear un objeto con new y la plataforma .NET se encargará automáticamente de
instanciar correctamente el objeto, independientemente de si es un objeto local o un objeto
remoto:

RemotingConfiguration.Configure ("Cliente.config");

Servidor remoto = new Servidor();

Más fácil, imposible !!!


Ejemplo

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 {

public class Servidor: System.MarshalByRefObject


{
public Servidor() {
Console.WriteLine("Constructor");
}

~Servidor() {
Console.WriteLine("Destructor");
}

public string GetHost () {


return Dns.GetHostName();
}

public string GetApplication () {


return RemotingConfiguration.ApplicationName;
}

public string GetAppDomain () {


return AppDomain.CurrentDomain.FriendlyName;
}
}
}

Este servidor lo alojaremos en un proceso al que se accederá remotamente ( Host.exe):

using System;
using System.Runtime.Remoting;

namespace RemotingExample
{
class Host {

static void Main(string[] args) {

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

<?xml version="1.0" encoding="utf-8" ?>


<configuration>
<system.runtime.remoting>
<application name="RemotingExample">

<service>
<wellknown mode="SingleCall"
type="RemotingExample.Servidor, Servidor"
objectUri="RemotingExample.rem" />
</service>

<channels>
<channel port="7777" ref="tcp" />
</channels>

</application>
</system.runtime.remoting>
</configuration>

De forma análoga, para el cliente también necesitamos otro fichero de configuración en


formato XML:

Cliente.config

<?xml version="1.0" encoding="utf-8" ?>


<configuration>
<system.runtime.remoting>
<application>

<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" );

Servidor remoto = new Servidor();

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.

.NET Remoting también en IIS

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).

Del mismo modo que la proliferación de PCs relativamente económicos y la disponibilidad de


ancho de banda dio lugar a la expansión de Internet y a la aparición de las aplicaciones web,
en las que los usuarios interactúan con las máquinas a través de navegadores web, se
pretende que el uso de XML y SOAP facilite el desarrollo de sistemas distribuidos
decentralizados en entornos heterogéneos. Los pilares sobre los que se construyen los
servicios web (XML y SOAP) están diseñados para facilitar la comunicación entre distintos
sistemas y aplicaciones sin la intervención de operadores humanos.

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 [Simple Object Access Protocol]

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>

La respuesta tiene el mismo formato que la solicitud:

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>

El formato de un mensaje SOAP es bastante simple. El mensaje en sí es como un sobre


(<Envelope>) con una cabecera y un cuerpo. La cabecera del mensaje SOAP (<Header>)
puede incluir información acerca de cómo ha de procesarse el mensaje en los sistemas
intermedios por los que vaya pasando (actores, según la terminología empleada en SOAP). El
cuerpo del mensaje es el que incluye los datos en sí, representados como un documento XML.

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.

HTTP/1.1 POST /soap/myservice


Content-Type: text/xml
SOAPAction: MyInterface#MyComponentMethod

<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

DISCO permite inspeccionar los servicios web existentes en un servidor concreto.


Básicamente, los documentos DISCO disponibles en un servidor web no son más que
conjuntos de enlaces a especificaciones WSDL y a otros documentos DISCO.

UDDI [Universal Description, Discovery, and Integration]

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

La creación de un servicio web ASP.NET requiere realizar las siguientes tareas:

 Tener instalada la plataforma .NET y el Internet Information Server. Si tenemos el


IIS funcionando pero no funcionan bien las páginas ASP.NET debemos ejecutar la utilidad
aspnet_regiis:

\Windows\Microsoft.NET\Framework\v...\aspnet_regiis -i

 Crear un directorio virtual en IIS en donde alojar nuestra aplicación ASP.NET:


Internet Services Manager > Default Web Site > Actions > Add a new virtual directory.
 Crear un fichero con extensión .asmx en el directorio virtual recién creado. En dicho
fichero se implementerá el servicio web:

<%@ WebService Class="Eco" Language="C#" %>

using System;
using System.Web.Services;

public class Eco


{
[WebMethod]
public string Eco ( string mensaje)
{
return mensaje;
}
}

Obviamente, en un caso real no implementaremos nuestro servicio web directamente en el


fichero .asmx, sino que utilizaremos la directiva Codebehind (como en las páginas ASP.NET).

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:

 Creamos un nuevo "sitio web" de tipo Servicio Web ASP.NET:

 El Visual Studio se encarga de crear adecuadamente un proyecto del tipo


seleccionado:

 A continuación, le ponemos el nombre que queramos a nuestro servicio web, para lo


que podemos usar la opción "Cambiar nombre..." que aparece en el menú contextual
"Refactorizar":
También podemos aprovechar para cambiar los nombres de los ficheros de nuestro
proyecto: EchoService.asmx y EchoService.cs

 Finalmente, implementamos la lógica de nuestro servicio web (trivial en este


ejemplo):
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].

Este es el servicio web que hemos creado:

El fichero EchoService.asmx simplemente incluye una directiva que hace


referencia al fichero de código:
<%@ WebService Language="C#"
CodeBehind="~/App_Code/EchoService.cs"
Class="EchoService" %>

En el fichero App_Code/EchoService.cs es donde encontramos el servicio web


en sí:

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

Prueba del servicio web en Visual Studio 2005

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):

La invocación de un servicio web, en .NET, se puede realizar igual que si utilizásemos un


formulario HTML, pasándole los parámetros mediante una solicitud HTTP POST. Obviamente,
también podremos utilizar siempre el protocolo SOAP.

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:

 Creamos un proyecto de tipo ASP.NET Web Service:

 El Visual Studio se encarga de configurar adecuadamente el servidor web (IIS)::


 A continuación, le ponemos el nombre que queramos a nuestro servicio web:

 Finalmente, implementamos la lógica de nuestro servicio web (trivial en este


ejemplo):

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].

Código fuente del ejemplo


Prueba del servicio web en Visual Studio 2003

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.

Acerca del uso de proxies...


En ocasiones, desde nuestra red local no podremos acceder a un servicio web
disponible en un servidor remoto si nuestro acceso al exterior requiere el uso de
proxies. Si para acceder al exterior de nuestra red tenemos que utilizar
necesariamente un proxy, debemos recurrir a la clase System.Net.Proxy para
indicar en nuestra aplicación que todo el tráfico dirigido al exterior de nuestra red
pase por el proxy que indiquemos.

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;
...

WebService service = ...

service.Proxy = new WebProxy("http://stargate.ugr.es:3128/",true);

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.

Uso desde Java

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.

Por ejemplo, podemos acceder a un servicio web ASP.NET utilizando el lenguaje de


programación Java. A continuación mostramos cómo se puede acceder a un servicio web
ASP.NET utilizando Apache Axis [Apache Extensible Interaction Engine], un conjunto de
paquetes que implementan los interfaces estándar en Java para usar servicios web:
import org.apache.axis.client.Call;
import javax.xml.rpc.Service;
import javax.xml.rpc.ServiceFactory;
import javax.xml.rpc.encoding.XMLType;
import javax.xml.namespace.QName;

public class Client


{
public static void main(String [] args)
{
try {

// Punto de acceso al servicio web

String endpointURL =
"http://elvex.ugr.es:800/WebService/Service.asmx";
String namespace = "http://elvex.ugr.es/";
String methodName;

ServiceFactory factory = ServiceFactory.newInstance();


Service service = factory.createService(new
QName(namespace,methodName));

// Servicio simple sin parámetros

methodName = "Hola";

Call call = (Call) service.createCall();


call.setTargetEndpointAddress(endpointURL);
call.setOperationName( new QName(namespace,methodName) );
call.setProperty( Call.SOAPACTION_URI_PROPERTY,
namespace+methodName);

String response = (String) call.invoke( new Object[] {} );


System.out.println(response);

// Servicio de eco

methodName = "Eco";

Call call3 = (Call) service.createCall();


call3.setTargetEndpointAddress(endpointURL);
call3.setOperationName( new QName(namespace,methodName) );
call3.setProperty( Call.SOAPACTION_URI_PROPERTY,
namespace+methodName );
call3.addParameter( new QName(namespace,"echo"),
XMLType.XSD_STRING,
javax.xml.rpc.ParameterMode.IN);
call3.setReturnType(XMLType.XSD_STRING);

System.out.println(
call3.invoke( new Object[] {"Prueba de eco... OK" } ));

// Servicio de conversión de temperaturas

methodName = "Convert";

Call call2 = (Call) service.createCall();


call2.setTargetEndpointAddress(endpointURL);
call2.setOperationName( new QName(namespace,methodName) );
call2.setProperty( Call.SOAPACTION_URI_PROPERTY,
namespace+methodName);
call2.addParameter( new QName(namespace,"tempFahrenheit"),
XMLType.XSD_INT,
javax.xml.rpc.ParameterMode.IN);
call2.setReturnType(XMLType.XSD_INT);

Integer iresponse;

iresponse = (Integer) call2.invoke( new Object[] { new Long(100)


} );
System.out.println(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:

Componente Propiedad Valor


DOCUMENT title Cliente para Google
Image (ID) Logo
ImageUrl logo.gif
TextBox (ID) TextBoxSearch
Button (ID) ButtonSearch
Text Buscar...

Al cargar la página comprobamos si tenemos que hacer una búsqueda:

private void Page_Load(object sender, System.EventArgs e)


{
string query;

if ( Page.IsPostBack )
query = TextBoxSearch.Text;
else
query = Request.Params["query"];

if ((query!=null) && (query.Length>0))


Search(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

string key = "9hKEFf1QFHJ73rgLZe3Pe9BWYofMTkU/"; // Clave de acceso


int start = 0;
int maxResults = 10;
bool filter = true; // Elimina resultados similares
string restrict = ""; // Restringe la búsqueda a un país, a un tema...
bool safeSearch = true; // Elimina contenido para adultos
string lr = "lang_es"; // Idioma

result = service.doGoogleSearch
(key, query, start, maxResults, filter, restrict, safeSearch, lr, "", "");

// Resultado

MemoryStream stream = new MemoryStream();


XmlTextWriter writer = new XmlTextWriter(stream,null);

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");

foreach (ResultElement element in result.resultElements) {

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();

// Transformación XSLT: XML -> HTML

XmlDocument xml = new XmlDocument();


XslTransform xslt = new XslTransform();
MemoryStream html = new MemoryStream();

stream.Position = 0;

xml.Load(stream);
xslt.Load(Server.MapPath("search.xsl"));
xslt.Transform(xml,null,html);

// Salida final

html.Position=0;

StreamReader streamReader = new StreamReader(html);


char[] buffer = new char[html.Length];
streamReader.Read ( buffer, 0, (int) html.Length );

Response.Write( buffer, 0, (int) html.Length );


}

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:

<?xml version="1.0" encoding="ISO-8859-1"?>


<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html"/>
<xsl:template match="SearchResults">

<xsl:if test="@estimatedTotalResultsCount = 0" >


<center><b>
No se encontraron documentos con
"<xsl:value-of select="@searchQuery"/>".
</b></center>
</xsl:if>

<xsl:if test="@estimatedTotalResultsCount > 0" >


<xsl:if test="string(@searchQuery)">

<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>

<xsl:value-of disable-output-escaping="yes" select="title"/>

</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:

wsdl /o:AmazonWebServices.cs AWSECommerceService.wsdl

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;

si no estamos usando un espacio de nombres predeterminado para el proyecto actual.

Aparte de los controles de nuestra interfaz, necesitaremos las siguientes variables de instancia
en nuestro formulario ASP.NET:

// ISBN del libro mostrado


private string isbn;

// Servicio web
AWSECommerceService service;

// Cesta de la compra
Cart cart;

Consulta del catálogo de productos

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:

 El ISBN lo obtenemos de la solicitud HTTP. Si ésta no incluye ningún ISBN, utilizamos


uno por defecto.
 Para acceder a cualquier servicio web de Amazon, siempre tendremos que
identificarnos (usando AssociateTag y AWSAccessKeyId, dos valores que nos dan al
registrarnos como usuarios de los servicios web de Amazon, en todos y cada uno de los
accesos que hagamos a los servicios web de Amazon).
 Una vez realizada una consulta para obtener los datos del libro cuyo ISBN hayamos
especificado, actualizamos la interfaz de usuario de acuerdo con los datos leídos (URL de
imagen de la portada, título del libro, autor/es y precio).
 Por último, no podemos olvidarnos de mantener el estado actual del carrito de la
compra de cada usuario como una variable de sesión, para lo cual utilizamos la colección
Session.

protected void Page_Load(object sender, EventArgs e)


{
// ISBN del libro

isbn = (Request["isbn"] == null) ? String.Empty : Request["isbn"];

if (isbn == String.Empty)
isbn = "0321113594";

// Servicio web de Amazon

service = new AWSECommerceService();

// Solicitud

ItemSearchRequest request = new ItemSearchRequest();

// Parámetros de la búsqueda
request.SearchIndex = "Books";
request.Power = "ISBN: " + isbn;

request.MerchantId = "Amazon"; // ofertas sólo de Amazon.com

// Resultados deseados

request.ResponseGroup = new string[] { "ItemAttributes", "Images",


"Offers" };

// Búsqueda de artículos

ItemSearch search = new ItemSearch();

search.AssociateTag = "ikorbooks-20";
search.AWSAccessKeyId = "0PDW1BZN4N11TAXXS6G2";
search.Request = new ItemSearchRequest[] { request };

// Ejecución de la consulta

ItemSearchResponse response = service.ItemSearch(search);

// Resultado de la consulta

Item book = response.Items[0].Item[0];

// Datos del libro

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 = "";

for (i = 0; i < autores; i++) {

LabelAutor.Text += book.ItemAttributes.Author[i];

if (i < autores - 2) {
LabelAutor.Text += ", ";
} else if (i == autores - 2) {
LabelAutor.Text += " & ";
}
}

LabelPrecio.Text = "Precio: "


+ book.Offers.Offer[0].OfferListing[0].Price.FormattedPrice;
// + book.ItemAttributes.ListPrice.FormattedPrice;

// 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:

 Para acceder al servicio X, en primer lugar, hemos de preparar la solicitud adecuada,


con nombre XRequest.
 A continuación, creamos un objeto de tipo X, que usamos para identificarnos (con
AssociateTag y AWSAccessKeyId) y acceder al servicio X pasándole como parámetro
los objetos de tipo XRequest que representan nuestras solicitudes (aunque no lo
hayamos hecho, podríamos ejecutar varias a la vez).
 La ejecución de la consulta nos da como resultado un objeto de tipo XResponse, que
contiene las colecciones de Items obtenidas como respuesta a nuestras solicitudes de
tipo XRequest.

En el caso de la búsqueda de productos en el catálogo de Amazon (la operación ItemSearch


ofrecida como parte de la interfaz pública del servicio A2S [Amazon Associates Service]),
podemos realizar búsquedas de distintos tipos: por título, por autor, por temas... En este caso,
hemos optado por realizar la búsqueda de un libro concreto usando su ISBN, usando "power
search" (propiedad Power) en el índice "Books" (propiedad SearchIndex).

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..

En nuestras aplicación, no obstante, hemos restringido la búsqueda a productos


proporcionados directamente por Amazon (de ahí lo de request.MerchantId = "Amazon")
para poder averiguar fácilmente el precio real al que Amazon vende cada producto:

 book.ItemAttributes.ListPrice nos da el precio de catálogo del producto.


 book.OfferSummary.LowestNewPrice nos devuelve el precio más barato al que
podemos comprar el producto nuevo.
 book.OfferSummary.LowestUsedPrice nos indica el precio más barato al que
podemos comprar el producto (usado).
 book.Offers.Offer[0].OfferListing[0].Price nos da el precio del producto
ofertado por Amazon al haber restringido la búsqueda con MerchantId = "Amazon". Si
no lo hubiésemos hecho, Offers podría incluir ofertas de más proveedores diferentes.

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.

 Lo primero que hacemos es comprobar si el usuario ya tiene una cesta de la compra.


Si no la tiene, la creamos con CartCreate y, si ya la tiene, todas nuestras solicitudes de
tipo CartAdd deben incluir el identificador de su cesta de la compra ( CartId) y un
código de autentificación complementario (HMAC, keyed-Hash Message Authentication
Code).
 Como siempre, en cada solicitud que hagamos a un servicio web de Amazon,
debemos acordarnos de identificarnos (mediante las propiedades AssociateTag y
AWSAccessKeyId de la operación correspondiente).
 Por último, cada vez que el usuario realice una operación sobre su cesta de la
compra, tenemos que actualizar adecuadamente la interfaz de usuario de nuestra
aplicación para que ésta muestre el estado de la compra del usuario (y guardar el carrito
de la compra del usuario como una variable de sesión).

protected void Button_Click(object sender, EventArgs e)


{
if (cart == null) {

CartCreateRequestItem cartCreateRequestItem = new


CartCreateRequestItem();

cartCreateRequestItem.ASIN = isbn;
cartCreateRequestItem.Quantity = "1";
cartCreateRequestItem.AssociateTag = "ikorbooks-20";

CartCreateRequest request = new CartCreateRequest();

request.Items = new CartCreateRequestItem[] { cartCreateRequestItem


};

CartCreate create = new CartCreate();

create.AssociateTag = "ikorbooks-20";
create.AWSAccessKeyId = "0PDW1BZN4N11TAXXS6G2";
create.Request = new CartCreateRequest[] { request };

CartCreateResponse response = service.CartCreate(create);

cart = response.Cart[0];

} else {

CartAddRequestItem cartAddRequestItem = new CartAddRequestItem();

cartAddRequestItem.ASIN = isbn;
cartAddRequestItem.Quantity = "1";
cartAddRequestItem.AssociateTag = "ikorbooks-20";

CartAddRequest request = new CartAddRequest();

request.Items = new CartAddRequestItem[] { cartAddRequestItem };


request.CartId = cart.CartId;
request.HMAC = cart.HMAC;

CartAdd add = new CartAdd();

add.AssociateTag = "ikorbooks-20";
add.AWSAccessKeyId = "0PDW1BZN4N11TAXXS6G2";
add.Request = new CartAddRequest[] { request };

CartAddResponse response = service.CartAdd(add);

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:

protected void ButtonCheckOut_Click(object sender, EventArgs e)


{
if (cart != null && cart.CartItems.CartItem.Length > 0) {
Response.Redirect(cart.PurchaseURL);
} else {
LabelCompra.Text = "No tiene ningún artículo en su cesta
de la compra";
LabelCompra.ForeColor = System.Drawing.Color.Red;
}
}

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

Capas en el diseño de software


Referencias

La división de un sistema en distintas capas o niveles de abstracción es una de las técnicas


más comunes empleadas por los ingenieros para construir sistemas complejos.

Esta división se puede apreciar en el hardware, donde el diseño de un sistema en un lenguaje


de alto nivel como VHDL o Verilog se traduce en un diseño a nivel de registros lógicos (RTL);
éste se implementa mediante puertas lógicas, a partir de las cuales se obtiene un diseño a
nivel de transistores; los transistores, finalmente, se crean en un circuito integrado con una
serie de máscaras.

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.

En realidad, el uso de capas es una forma más de la técnica de resolución de problemas


conocida con el nombre de "divide y vencerás", que se basa en descomponer un problema
complejo en una serie de problemas más sencillos de forma que se pueda obtener la solución
al problema complejo a partir de las soluciones a los problemas más sencillos. Al dividir un
sistema en capas, cada capa puede tratarse de forma independiente (sin tener que conocer los
detalles de las demás).

Desde el punto de vista de la Ingeniería del Software, la división de un sistema en capas


facilita el diseño modular (cada capa encapsula un aspecto concreto del sistema) y permite la
construcción de sistemas débilmente acoplados (si minimizamos las dependencias entre capas,
resultará más fácil sustituir la implementación de una capa sin afectar al resto del sistema).
Además, el uso de capas también fomenta la reutilización (p.ej. TCP/IP se utiliza en una amplia
variedad de aplicaciones, desde HTTP y FTP hasta telnet y SSH).
Capas en el diseño de software
Como es lógico, la parte más difícil en la construcción de un sistema multicapa es decidir
cuántas capas utilizar y qué responsabilidades asignarle a cada capa.

En las arquiecturas cliente/servidor se suelen utilizar dos capas. En el caso de las


aplicaciones informáticas de gestión, esto se suele traducir en un servidor de bases de datos
en el que se almacenan los datos y una aplicación cliente que contiene la interfaz de usuario y
la lógica de la aplicació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.

Mantener la misma arquitectura y pasar la lógica de la aplicación al servidor tampoco resulta


una solución demasiado acertada. Se puede implementar la lógica de la aplicación utilizando
procedimientos almacenados, pero éstos suelen tener que implementarse en lenguajes
estructurados no demasiado versátiles. Además, suelen ser lenguajes específicos para cada
tipo de base de datos, por lo que la portabilidad del sistema se ve gravemente afectada.

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:

 La capa de presentación, encargada de interactuar con el usuario de la


aplicación mediante una interfaz de usuario (ya sea una interfaz web, una
interfaz Windows o una interfaz en línea de comandos, aunque esto útlimo
suele ser menos habitual en la actualidad).
 La lógica de la aplicación [a la que se suele hacer referencia como business
logic o domain logic], usualmente implementada utilizando un modelo
orientado a objetos del dominio de la aplicación, es la responsable de
realizar las tareas para las cuales se diseña el sistema.
 La capa de acceso a los datos, encargada de gestionar el almacenamiento
de los datos, generalmente en un sistema gestor de bases de datos
relacionales, y de la comunicación del sistema con cualquier otro sistema
que realice tareas auxiliares (p.ej. middleware).

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:

LAYER vs. TIER


Aunque ambos términos se traduzcan igual en castellano, el término
tier suele hacer referencia a las distintas capas utilizadas en el
despliegue de una aplicación a nivel físico, mientras que el término
layer se emplea de un modo más abstracto para hacer referencia a la
descomposición de cualquier sistema (aunque éste se implemente en
un único tier).

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

Organización de la lógica de la aplicación


Una vez que hemos separado el núcleo de la aplicación de sus distintos interfaces, aún nos
queda por decidir cómo vamos a organizar la implementación de la lógica asociada a la
aplicación. Como en cualquier otra tarea de diseño, tenemos que llegar a un compromiso
adecuado entre distintos intereses. Por un lado, nos gustaría que el diseño resultante fuese lo
más sencillo posible. Por otro lado, sería deseable que nuestro diseño estuviese bien preparado
para soportar las modificaciones que hayan de realizarse en el futuro.

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:

 Rutinas: La forma más simple de implementar cualquier sistema se basa en


implementar procedimientos y funciones que acepten y validen las entradas
recibidas de la capa de presentación, realicen los cálculos necesarios,
utilicen los servicios de aquellos sistemas que hagan falta para completar la
operación, almacenen los datos en las bases de datos y envíen una
respuesta adecuada al usuario. Básicamente, cada acción que el usuario
pueda realizar se traducirá en un procedimiento que realizará todo lo que
sea necesario al más puro estilo del diseño estructurado tradicional. Aunque
este modelo sea simple y pueda resultar adecuado a pequeña escala, la
evolución de las aplicaciones diseñadas de esta forma suele acabar siendo
una pesadilla para las personas encargadas de su mantenimiento.
 Módulos de datos: Ante la situación descrita en el párrafo anterior, lo usual
es dividir el sistema utilizando los distintos conjuntos de datos con los que
trabaja la aplicación para crear módulos más o menos independientes. De
esta forma, se facilita la eliminación de lógica duplicada. De hecho, muchos
de los entornos de desarrollo visual de aplicaciones permiten definir
módulos de datos que encapsulen los conjuntos de datos con los que se
trabaja y la lógica asociada a ellos. Las herramientas de Borland, Delphi y
C++Builder, son un claro ejemplo. Microsoft, en su arquitectura DNA
[Distributed interNet Application], fomenta este estilo al emplear conjuntos
de datos (resultado de ejecutar consultas SQL) sobre los cuales operan
directamente las distintas capas de una aplicación multicapa. En el caso de
la plataforma .NET, la clase DataSet proporciona la base sobre la que se
montaría todo el diseño de una aplicación (algo que obviamente facilita el
Visual Studio .NET).
 Modelo del dominio: Una tercera opción, la ideal para cualquier purista de la
orientación a objetos, es crear un modelo orientado a objetos del dominio
de la aplicación. En vez de que una rutina se encargue de todo lo que haya
que hacer para completar una acción, cada objeto es responsable de
realizar las tareas que le atañen directamente.

En la práctica, no todo es blanco o negro. Aunque empleemos un modelo orientado a objetos


del dominio de la aplicación, es habitual crear una capa intermedia entre la capa de
presentación y la lógica de la aplicación a la que se suele denominar capa de servicio. La
interfaz de la capa de servicio incluirá métodos asociados a las distintas acciones que pueda
realizar el usuario, si bien, en vez de incluir en ella la lógica de la aplicación, la capa de
servicio delega inmediatamente en los objetos responsables de cada tarea. En cierto modo, la
capa de servicio se encarga de la lógica específica de la aplicación, dejando para el modelo del
dominio la lógica del dominio (común a cualquier aplicación que se construya sobre el mismo
dominio de aplicación).
Cualquier aplicación sufre modificaciones de mayor o menor importancia a lo largo de su vida
útil. Dichas modificaciones alteran el diseño inicial de la aplicación y tienden a aumentar su
entropía. Si utilizamos rutinas para implementar la lógica de nuestra aplicación en el sentido
tradicional, las modificaciones pueden suponer tener que revisar el código completo de la
aplicación para descubrir lo que hay que actualizar. Es como si tuviésemos una habitación
completamente desordenada en la que hay que encontrar algo en particular (y ya sabemos lo
que las madres suelen decir acerca de eso). En el caso de los módulos de datos, el impacto de
las modificaciones suele ser más fácil de determinar pero, aún así, podemos encontrarnos con
sorpresas desagradables si todos los módulos de nuestra aplicación trabajan sobre un conjunto
de datos cuya estructura debemos alterar ligeramente. Por último, si diseñamos un buen
modelo orientado a objetos, la encapsulación proporcionada por los objetos de nuestra
aplicación permitirá que el impacto de las modificaciones sea de carácter local en la mayoría
de las ocasiones. En cierto modo, estamos limitando el aumento de la entropía al interior de
los cajones (algo que la mayoría de nosotros tolera sin demasiados problemas). Para llegar a
este punto, no obstante, primero hemos de ser capaces de crear un buen modelo orientado a
objetos...
Buenas costumbres en el desarrollo de software
En Ingeniería del Software se han identificado distintas prácticas que suelen considerarse muy
útiles en el desarrollo de aplicaciones. A continuación veremos algunas que nos ayudarán a
mejorar nuestros hábitos en el desarrollo de software:

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.

Además, la existencia de catálogos de patrones nos permite aprovechar la experiencia de otros


diseñadores y mejorar nuestras habilidades en el diseño de software. En estos catálogos, los
patrones se representan en un formato estándar (problema-solución-consecuencias) que
codifica de una forma digerible las soluciones a problemas recurrentes que diseñadores
expertos han descubierto en un contexto dado.

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.

La refactorización no es una actividad más a la que se le reserva un espacio en la planificación


de un proyecto, sino que es algo que se va haciendo sobre la marcha conforme hace falta.
Refactorizar es extremadamente útil ya que nos permite eliminar redundancias cuando se
encuentra lógica duplicada en una aplicación. También puede ser útil cuando tenemos que
añadir funcionalidad a una aplicación, pues nos sirve de paso inicial de preparación para que el
código resulte más fácil de entender y sea más fácil añadir la funcionalidad deseada a la
aplicación. Incluso puede resultar útil cuando estamos intentando depurar nuestra aplicación:
si la hacemos más fácil de entender, será más fácil encontrar ese molesto error que se nos
resiste.

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).

Gestión de la configuración del software

La gestión de la configuración del software [SCM: Software Configuration Management], más


conocida vulgarmente por uno de sus aspectos, el control de versiones, es una actividad clave
en la gestión del proceso de desarrollo de software. De hecho, aparece como área clave para
obtener el nivel 2 del CMM [Capability Maturity Model], modelo en el cual el nivel 1 representa
la anarquía en el proceso de desarrollo de software.

Independientemente de su importancia en el control del proceso de desarrollo de software


(algo innegable) y de su utilidad a la hora de evitar pérdidas irreparables (siempre y cuando se
hagan copias de seguridad de todos los componentes del software), las herramientas de
control de versiones son esenciales para que podamos refactorizar sin piedad el código sobre
el que estemos trabajando en cada momento. Si, por cualquier motivo, al terminar las
modificaciones que hagamos no obtenemos lo que esperábamos, siempre podemos volver
cómodamente a una versión anterior de nuestro código y cambiar de estrategia sin haber
perdido nada, ya que el tiempo empleado no se habrá perdido del todo si nos ha servido para
aprender algo.

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:

 La clase Document sirve para representar los documentos del catálogo de la


biblioteca, los cuales se identifican por un código alfanumérico ( asin).
 La clase Author es una clase auxiliar empleada para mantener información
acerca de los autores de los documentos.
 La clase Category se emplea para clasificar los documentos por temas,
pudiendo establecer jerarquías de categorías gracias a la asociación
involutiva que aparece en el diagrama.
 La clase Review nos permite almacenar comentarios y críticas relativos a los
documentos de la bibliteca.
 Dichos comentarios los podrán escribir los usuarios registrados de nuestra
biblioteca (representados por la clase Reviewer).

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).

Comenzamos por la clase más simple:


public class Author
{
private string name;

public Author (string name)


{
this.name = name;
}

public string Name {


get { return name; }
}
}

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:

public class Category


{
private string description;
private ArrayList super;
private ArrayList sub;

public Category (string description)


{
this.description = description;
this.super = new ArrayList();
this.sub = new ArrayList();
}

public string Description {


get { return description; }
}

public Category[] SuperCategories {


get { return (Category[]) super.ToArray (); }
}

public Category[] SubCategories {


get { return (Category[]) sub.ToArray(); }
}

public bool isSuperCategory (Category category) {


return super.Contains(category);
}

public bool isSubCategory (Category category) {


return sub.Contains(category);
}

public void AddSuperCategory (Category category) {


super.Add(category);
category.sub.Add(this);
}

public void RemoveSuperCategory (Category category) {


super.Remove(category);
category.sub.Remove(this);
}
public void AddSubCategory (Category category) {
category.AddSuperCategory(this);
}

public void RemoveSubCategory (Category category) {


category.RemoveSuperCategory(this);
}
}

A partir de las dos clases anteriores es casi inmediata la implementación de la clase


Document, la clase central de nuestra biblioteca digital:

public class Document


{
private string asin;
private string title;
private string source;
private ArrayList authors;
private ArrayList categories;

public Document(string asin, string title)


{
this.asin = asin;
this.title = title;
this.authors = new ArrayList();
this.categories = new ArrayList();
}

public string ASIN {


get { return asin; }
}

public string Title {


get { return title; }
set { title = value; }
}

public string Source {


get { return source; }
set { source = value; }
}

// Autores

public Author[] Authors {


get { return (Author[]) authors.ToArray(
typeof(Author) ); }
}

public bool isAuthor (Author author) {


return authors.Contains(author);
}

public void AddAuthor (Author author) {


authors.Add (author);
}

public void RemoveAuthor (Author author) {


authors.Remove (author);
}

// Categorías

public Category[] Categories {


get { return (Category[]) categories.ToArray(
typeof(Category) ); }
}

public bool isCategory (Category category) {


return categories.Contains(category);
}

public void AddCategory (Category category) {


categories.Add (category);
}

public void RemoveCategory (Category category) {


categories.Remove(category);
}
}

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:

public class Reviewer


{
private string login;

public Reviewer (string login)


{
this.login = login;
}

public string Login {


get { return login; }
}

public void changePassword (string oldPassword, string


newPassword)
{
if ( checkPassword(oldPassword) ) {
Password = newPassword;
} else {
throw new SecurityException("Clave incorrecta");
}
}

// Gestión de la clave de acceso

public bool checkPassword (string password) {


return true; // Aquí se comprobaría si la clave es
correcta
}

private string Password {


set { } // Aquí se establecería la clave del usuario
}
}
public class SecurityException :
System.ApplicationException {
public SecurityException(string msg) :base(msg) { }
}

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 class Review


{
private string text;
private Reviewer reviewer;
private Document document;
private DateTime date;
private Rating rating;

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;
}

public Reviewer Reviewer {


get { return reviewer; }
}

public Document Document {


get { return document; }
}

public string Text {


get { return text; }
}

public DateTime Date {


get { return date; }
}

public Rating Rating {


get { return rating; }
}
}

/// Posibles evaluaciones

public enum Rating {


Excellent,
Good,
Fair,
Bad
}

En este caso, hemos definido una enumeración para especificar el conjunto de valores
permitido al evaluar un documento.

Implementación inicial de las clases de la biblioteca digital (Visual Studio 2003)


Implementación inicial de las clases de la biblioteca digital (Visual Studio 2005)
Importante
Obsérvense las distintas estrategias que se han utilizado para
implementar en C# las diferentes asociaciones que aparecen en el
diagrama de clases, especialmente la asociación bidireccional que nos
sirve para definir jerarquías de categorías.

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);

Assert.AreEqual (0, software.SuperCategories.Length


);
Assert.AreEqual (1, software.SubCategories.Length );
}
}
}
Nota
En versiones anteriores de NUnit, en vez de utilizar el método
Assert.AreEqual, habríamos escrito Assertion.AssertEquals.
Del mismo modo, mientras que antes escribiríamos
Assertion.Assert, ahora usaremos el método Assert.IsTrue.
Las clases que implementan pruebas de unidad con NUnit deben tener el atributo
[TestFixture] y deben ser públicas (para poder acceder a ellas desde fuera y poder
ejecutar las pruebas de unidad).

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] );
}
}
}

La clase anterior muestra cómo podemos implementar un conjunto de tests a partir de un


conjunto de objetos cuya configuración inicial establecemos en el método etiquetado con el
atributo [SetUp].

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.

Ejemplo de uso de NUnit (Visual Studio 2003, con Assertion)


Ejemplo de uso de NUnit (Visual Studio 2005, con Assert)

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).

Herramientas de control de versiones existen muchísimas, algunas de las cuales se integran


perfectamente en el entorno de desarrollo. Por ejemplo, podemos utilizar la herramienta Visual
SourceSafe de Microsoft. Una vez que tengamos esta herramienta instalada, lo único que
tenemos que hacer es seleccionar la opción "Agregar solución al control de código fuente...", la
cual aparece en el menú contextual del "Explorador de soluciones" y en el submenú "Control
de código fuente" del menú Archivo en la ventana principal del Visual Studio .NET:
Al activar el control de versiones para nuestra solución, debemos acceder a una de las bases
de datos de nuestra herramienta de control de versiones, para lo cual deberemos
identificarnos correctamente:

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:

De esta forma nos aseguramos de que el resultado de nuestro trabajo no se pierda:


Además, seleccionando la opción "Comparar..." (versiones) del menú contextual del Explorador
de soluciones, podemos ver y analizar las diferencias existentes entre distintas versiones de un
fichero, a las cuales podemos hacer referencia en Visual SourceSafe añadiéndole la
terminación ;v al nombre del fichero, donde v es el número de la versión del fichero:

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

Acceso a los datos


Existen distintas formas en las que una aplicación puede acceder a los datos, almacenados por
lo general en una base de datos relacional:

 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.

Correspondencia entre las clases y la base de datos


En primer lugar, hemos de establecer la correspondencia existente entre las clases de nuestro
modelo orientado a objetos del dominio de la aplicación y las tablas que almacenarán los datos
en un sistema gestor de bases de datos relacionales. Para ello, definiremos una serie de
atributos que nos permitirán mantener la correspondencia entre el modelo orientado a objetos
y la base de datos. Dichos atributos los definiremos dentro del espacio de nombres DB.ORmap.

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 {
...
}

Dicho atributo, aplicable a clases y structs, se implementa de la siguiente manera:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class DBTableAttribute : Attribute
{
string tableName;

public DBTableAttribute(string tableName)


{
this.tableName = tableName;
}

public string TableName


{
get { return tableName; }
set { tableName = value; }
}
}

Columnas y variables de instancia

Cada variable de instancia de la clase ha de traducirse a una columna en la tabla


correspondiente de la base de datos (o conjunto de columnas, según el caso). Para establecer
esta correspondencia podemos crear un atributo [DBColumn]:

[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;

public DBColumnAttribute(DbType type)


{
this.type = type;
}

public DBColumnAttribute(string columnName, DbType type)


{
this.columnName = columnName;
this.type = type;
}

public string ColumnName


{
get { return columnName; }
set { columnName = value; }
}

public DbType Type


{
get { return type; }
set { type = value; }
}

public int Size


{
get { return size; }
set { size = value; }
}

public bool Nullable {


get { return nullable; }
set { nullable = value; }
}
}

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;
...
}

La implementación del atributo [DBKey] es trivial:

[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;
}

La implementación de este último atributo es similar a la de los demás:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class DBForeignKeyAttribute : Attribute
{
private string columnName;

public DBForeignKeyAttribute ()
{
}

public DBForeignKeyAttribute (string columnName)


{
this.columnName = columnName;
}

public string ColumnName


{
get { return columnName; }
set { columnName = value; }
}
}

Los cuatro atributos anteriores ([DBTable], [DBColumn], [DBKey] y [DBForeignKey]) los


definiremos bajo el espacio de nombres DB.ORmap en una biblioteca de clases denominada
ORmap.dll.
Reflexionando sobre las clases
A continuación utilizaremos un mecanismo conocido con el nombre de reflexión para ser
capaces de explorar las clases de un assembly en tiempo de ejecución. En el caso de la
plataforma .NET, las clases necesarias para reflexionar se encuentran en el espacio de
nombres System.Reflection.

Creamos una nueva aplicación en línea de comandos y comenzamos su implementación


cargando un assembly que el usuario ha de especificar como parámetro:

class TestReflection
{
[STAThread]
static void Main(string[] args)
{
if (args.Length != 1) {
Console.WriteLine("Uso: TestReflection <assembly>");
return;
}

Assembly assembly = null;

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):

public static void ParseAssembly(Assembly assembly)


{
Type[] types = assembly.GetTypes();

foreach (Type type in types) {

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]:

public static DBTableAttribute getTable (Type type)


{
DBTableAttribute[] dataTable = (DBTableAttribute[])
type.GetCustomAttributes(typeof(DBTableAttribute),
true);

if (dataTable.Length > 0) {
return dataTable[0];
} else {
return null;
}
}

Finalmente, tenemos que analizar cada una de las clases por separado:

public static void ParseClass (Type type)


{
DBTableAttribute table = getTable(type);
FieldInfo[] fields = type.GetFields( BindingFlags.Public
| BindingFlags.NonPublic | BindingFlags.Instance
);

// Columnas

foreach (FieldInfo field in fields)


ParseMember(field,field.FieldType,table);

// Clave primaria

Console.Write (" Key: ");

foreach (FieldInfo field in fields)


if ( isKey(field) )
ParseKey(field,field.FieldType,table);
}

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:

public static bool isKey (MemberInfo member)


{
DBKeyAttribute[] key = (DBKeyAttribute[])
member.GetCustomAttributes(typeof(DBKeyAttribute), true);

return (key.Length > 0);


}

public static bool isForeignKey (MemberInfo member) {


DBForeignKeyAttribute[] key = (DBForeignKeyAttribute[])
member.GetCustomAttributes(typeof(DBForeignKeyAttribute),
true);
return (key.Length > 0);
}

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é):

public static void ParseMember


(MemberInfo member, Type type, DBTableAttribute table)
{
ParseMember(member,type,table,"");
}

public static void ParseMember


(MemberInfo member, Type type, DBTableAttribute table, string
prefix)
{
if ( !isForeignKey(member) ) {

DBColumnAttribute[] column = (DBColumnAttribute[])


member.GetCustomAttributes(typeof(DBColumnAttribute),
true);

if (column.Length > 0) {

DBColumnAttribute col = column[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:

public static void ParseKey


(MemberInfo member, Type type, DBTableAttribute table)
{
ParseKey(member,type,table,"");
}

public static void ParseKey


(MemberInfo member, Type type, DBTableAttribute table, string
prefix)
{
if ( !isForeignKey(member) ) {
Console.Write(" " + prefix + member.Name );
} else {
Console.Write(" [" + member.Name +"]");
ParseForeignKey (member.Name, type, table, prefix, new
Visitor(ParseKey));
}
}

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:

public delegate void Visitor


(MemberInfo member, Type type, DBTableAttribute table, string
prefix);

public static void ParseForeignKey


(string name, Type type, DBTableAttribute table, string prefix,
Visitor visitor)
{
string foreignTable;

if (type.IsClass) {
DBTableAttribute dataTable = getTable(type);

if (dataTable!=null) {
foreignTable = dataTable[0].TableName;
prefix = prefix+name+"_";

FieldInfo[] fields = type.GetFields


( BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance );

foreach (FieldInfo field in fields) {


if (isKey(field))
visitor(field,field.FieldType,table,prefix);
}
}
}
}

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:

 Las instancias de cada clase etiquetada con el atributo [DBTable] se almacenarán


en una tabla de la base de datos.
 Cada campo de la clase etiquetado con el atributo [DBColumn] dará lugar a una
columna en la tabla correspondiente de la base de datos.
 Todos los campos etiquetados con el atibuto [DBKey] formarán parte de la clave
primaria de la tabla derivada de la clase.
 La existencia de una clave externa, etiquetada con el atributo [DBForeignKey],
implicará la inclusión en la tabla de tantas columnas como hagan falta para hacer
referencia a la clave primaria de la tabla referenciada (la tabla que se deriva de la clase a
la que pertenece el atributo etiquetado con [DBForeignKey].

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;

[DBColumn("login", DbType.String, Size=16)]


public string login;

[DBColumn("passwd", DbType.String, Size=64)]


public string password;

[DBColumn("access", DbType.String, Size=16)]


public string priviledges;
}

[DBTable("DOCUMENT")]
public class Document {

[DBColumn("id",DbType.Int32)]
[DBKey]
public int id;

[DBColumn("title", DbType.String, Size=256)]


public string title;
[DBColumn("author", DbType.String, Size=256)]
public string author;
[DBColumn("source", DbType.String, Size=256)]
public string source;

[DBForeignKey]
public DocumentUser owner;
}

Estas dos clases dan lugar al siguiente esquema en la base de datos:


Clase 'User' [Tabla ACCESS]
· User.id [System.Int32] -> Columna id [Int32]
· User.login [System.String] -> Columna login [String]
· User.password [System.String] -> Columna passwd [String]
· User.priviledges [System.String] -> Columna access [String]
Key: id

Clase 'Document' [Tabla DOCUMENT]


· Document.id [System.Int32] -> Columna id [Int32]
· Document.title [System.String] -> Columna title [String]
· Document.author [System.String] -> Columna author [String]
· Document.source [System.String] -> Columna source [String]
· User.id [System.Int32] -> Columna owner_id [Int32]
Key: id
Del modelo orientado a objetos a la base de datos
Obviamente, el programa de la sección anterior que recorría las clases de un assembly para
descubrir cómo se deben almacenar los objetos en la base de datos es sólo un ejemplo
ilustrativo y no resulta demasiado útil en la realidad.

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:

model = new DatabaseObjectModel(type);


model.ToXMLFile(type.ToString()+".xml");

La clase DatabaseObjectModel nos servirá para representar la estructura de los objetos de


un tipo en la base de datos y nos será de gran utilidad para automatizar el acceso a la base de
datos en aplicaciones diseñadas utilizando un modelo orientado a objetos del dominio de la
aplicación.

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:

 La clase DatabaseColumn representa una columna de una tabla de la base de datos.


 La clase DatabaseColumns representa un conjunto de columnas pertenecientes a
una misma tabla de la base de datos.
 Finalmente, la clase DatabaseObjectModel nos servirá para modelar la
representación de los objetos de una clase en la base de datos.

DatabaseColumn

La implementación de esta clase es relativamente sencilla. Nos limitamos a encapsular una


serie de propiedades asociadas a una columna e implementamos una función que nos sirve
para obtener una representación en XML de los datos de los que disponemos:

public class DatabaseColumn


{
private MemberInfo member; // Miembro asociado de la clase
private string id; // QName
private string name; // Nombre de la columna
private DbType type; // Tipo de la columna
private int size; // Tamaño de la columna
private bool nullable; // ¿Puede ser NULL?

// 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

public string MemberPath {


get { return id; }
}

public string MemberName {


get { return member.Name; }
}

public Type MemberReflection {


get { return member.ReflectedType; }
}

public string ColumnName {


get { return name; }
set { name = value; }
}

public DbType ColumnType {


get { return type; }
}

// Conversión en una cadena

public override string ToString () {


return member.ReflectedType+"."+member.Name
+ " -> " + name + " [" + type + "]";
}

// Volcado de los datos en XML

public void ToXML (ref XmlTextWriter writer)


{
writer.WriteStartElement("column");

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:

public class DatabaseColumns


{
private string table; // Nombre de la tabla
private MemberInfo member; // Miembro asociado al
conjunto
private ArrayList fields; // Conjunto de columnas

// Constructores

public DatabaseColumns (string table)


: this(table,null)
{
}

public DatabaseColumns (string table, MemberInfo


member)
: this(table, member, new ArrayList() )
{
}

public DatabaseColumns (string table, MemberInfo


member, ArrayList list)
{
this.table = table;
this.member = member;
this.fields = list;
}

// Propiedades

public MemberInfo Member {


get { return member; }
set { member = value; }
}

public string Table {


get { return table; }
set { table = value; }
}

// Conjunto de columnas

internal ArrayList Columns {


get { return fields; }
}

public int Size {


get { return fields.Count; }
}
public DatabaseColumn this[int index]
{
get { return (DatabaseColumn) fields[index]; }
set { fields[index] = value; }
}

public void addColumn (DatabaseColumn column)


{
fields.Add (column);
}

internal void addColumns (ArrayList columns)


{
fields.AddRange(columns);
}

// Volcado de los datos en XML

public void ToXML (ref XmlTextWriter writer)


{
int i;

if (member!=null)

writer.WriteAttributeString("member",member.Name);

for (i=0; i<fields.Count; i++)


this[i].ToXML(ref writer);
}
}

DatabaseObjectModel

Finalmente, llegamos a la clase clave en la conversión del modelo orientado a objetos en un


modelo válido para una base de datos relacional. La implementación de esta clase comienza
con la declaración de variables de instancia:

/// <summary>
/// Modelo del objeto tal como se almacena en la base de
datos
/// </summary>

public class DatabaseObjectModel


{
private Type type;
private MemberInfo member;
private string table;
private DatabaseColumns key;
private DatabaseColumns data;
private ArrayList foreignKeys;
...

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>

public DatabaseObjectModel (Type type)


{
this.type = type;
this.table = getTable(type).TableName;

key = new DatabaseColumns ( table );


data = new DatabaseColumns ( table );
foreignKeys = new ArrayList();

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>

private DatabaseObjectModel (string table, MemberInfo


member)
{
this.member = member;
this.table = table;

key = new DatabaseColumns ( table );


data = new DatabaseColumns ( table );
foreignKeys = new ArrayList();
}

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.

public string Table {


get { return table; }
}

public DatabaseColumns Key {


get { return key; }
}

public DatabaseColumns Data {


get { return data; }
}

public DatabaseColumns getForeignKey (int index) {


return (DatabaseColumns) foreignKeys[index];
}

public int getForeignKeyCount () {


return foreignKeys.Count;
}

public void addForeignKey (DatabaseColumns key) {


foreignKeys.Add(key);
}

Análisis de una clase

A continuación nos encargamos de la parte central: la encargada de construir el modelo a


partir de los atributos con los que etiquetamos las clases de nuestra aplicación de una forma
similar a como lo hicimos en la sección anterior:

private const BindingFlags memberFlags = BindingFlags.Public


| BindingFlags.NonPublic
| BindingFlags.Instance;

// Análisis de la estructura de la clase

private void ParseClass (Type type)


{
// Miembros de la clase

FieldInfo[] fields = type.GetFields( memberFlags );

foreach (FieldInfo field in fields)


addColumns ( ParseMember(field,field.FieldType), isKey(field)
);

// Superclase

DBTableAttribute table = getTable(type);


DBTableAttribute super = getTable(type.BaseType);

if ((super!=null) && (super!=table))


addForeignKey( new DatabaseColumns( super.TableName, null,
Key.Columns ) );
}

private void addColumns (ArrayList list, bool key)


{
if (key)
Key.addColumns (list);
else
Data.addColumns (list);
}

// Análisis de un miembro de una clase

private ArrayList ParseMember (MemberInfo member, Type type)


{
return ParseMember(member,type, "");
}

private ArrayList ParseMember (MemberInfo member, Type type, string


prefix)
{
ArrayList list = new ArrayList();

if ( !isForeignKey(member) ) {

DBColumnAttribute column = getColumn(member);

if (column!=null)
list.Add( new DatabaseColumn (member, column, prefix ) );
} else {
list = ParseForeignKey (member, type, prefix, new
Visitor(ParseMember));
}

return list;
}

// Claves externas

private delegate ArrayList Visitor


(MemberInfo member, Type type, string prefix);

private ArrayList ParseForeignKey


(MemberInfo member, Type type, string prefix, Visitor
visitor)
{
string foreignTable;
ArrayList list = new ArrayList();

if (type.IsClass) {

DBTableAttribute table = getTable(type);

if (table!=null)
foreignTable = table.TableName;
else
foreignTable = null;

prefix += member.Name+"_";

// Columnas

FieldInfo[] fields = type.GetFields( memberFlags );

foreach (FieldInfo field in fields) {


if (isKey(field) || (foreignTable==null))
list.AddRange( visitor(field,field.FieldType,prefix) );
}

// 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:

public void ToXML (ref XmlTextWriter writer)


{
int i;

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();

for (i=0; i<getForeignKeyCount(); i++) {


writer.WriteStartElement("Foreign-Key");
writer.WriteAttributeString("table",getForeignKey(i).Table);
getForeignKey(i).ToXML(ref writer);
writer.WriteEndElement();
}

writer.WriteEndElement();
}

private void ToXMLDocument (ref XmlTextWriter writer)


{
writer.Formatting = Formatting.Indented;
writer.Indentation = 2;

writer.WriteStartDocument();
writer.WriteStartElement("DatabaseModel");
writer.WriteAttributeString("type", this.type.ToString());

ToXML(ref writer);

writer.WriteEndElement();
writer.WriteEndDocument();
}

public void ToXMLFile (string filename)


{
XmlTextWriter writer = new XmlTextWriter(filename,
System.Text.Encoding.GetEncoding("ISO-
8859-1"));
ToXMLDocument(ref writer);
writer.Close();
}

public override string ToString ()


{
StringWriter w = new StringWriter();
XmlTextWriter writer = new XmlTextWriter(w);
ToXMLDocument(ref writer);
writer.Flush();

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:

public static DBTableAttribute getTable (Type type) {


DBTableAttribute[] table = (DBTableAttribute[])
type.GetCustomAttributes(typeof(DBTableAttribute), true);

if (table.Length > 0) {
return table[0];
} else {
return null;
}
}

public static DBColumnAttribute getColumn (MemberInfo member) {


DBColumnAttribute[] column = (DBColumnAttribute[])
member.GetCustomAttributes(typeof(DBColumnAttribute), true);

if (column.Length > 0)
return column[0];
else
return null;
}

public static DBForeignKeyAttribute getFK (MemberInfo member) {


DBForeignKeyAttribute[] fk = (DBForeignKeyAttribute[])
member.GetCustomAttributes(typeof(DBForeignKeyAttribute),
true);

if (fk.Length > 0)
return fk[0];
else
return null;
}

public static bool isKey (MemberInfo member) {


DBKeyAttribute[] key = (DBKeyAttribute[])
member.GetCustomAttributes(typeof(DBKeyAttribute), true);

return (key.Length > 0);


}

public static bool isForeignKey (MemberInfo member) {


DBForeignKeyAttribute[] key = (DBForeignKeyAttribute[])
member.GetCustomAttributes(typeof(DBForeignKeyAttribute),
true);

return (key.Length > 0);


}

Creación automática de las tablas en la base de datos


Las clases descritas en los apartados anteriores nos pueden servir para automatizar la creación
de la base de datos a partir de un modelo orientado a objetos. De hecho, el modelo obtenido
por la clase DatabaseObjectModel no es más que una representación de la parte del
esquema de la base de datos relacionada con los objetos cuyo tipo se modela. Por tanto,
podemos derivar los CREATE TABLE necesarios para crear la tablas en la base de datos a
partir de la representación en XML del modelo obtenido con DatabaseObjectModel. Para ello
utilizaremos una hoja de estilo XSLT como la siguiente:

<?xml version="1.0" encoding="ISO-8859-1"?>

<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>

<!-- CREATE TABLE -->

<xsl:template match="Table">

CREATE TABLE <xsl:value-of select="@name" />


(
<xsl:apply-templates/>
<xsl:call-template name="pk" />
);
<xsl:call-template name="fk" />
</xsl:template>

<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>

<!-- Tipos de datos: SQL Server -->

<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):

<!-- Tipos de datos: 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.

Anexo: Aplicación de hojas de estilo XSLT

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;
}

string filename = args[0];


string stylesheet = args[1];

XslTransform xslt = new XslTransform();

xslt.Load(stylesheet);

XPathDocument xpathdocument = new XPathDocument(filename);


XmlTextWriter writer = new XmlTextWriter(Console.Out);
writer.Formatting=Formatting.Indented;

xslt.Transform(xpathdocument, null, writer, null);


}
}
}
De la base de datos al modelo orientado a objetos
Hasta ahora hemos automatizado la conversión del modelo orientado a objetos al modelo
relacional típico de muchos gestores de bases de datos. Sin embargo, aún no hemos visto
cómo podemos aprovehar lo que ya tenemos hecho para simplificar el desarrollo en sí de las
aplicaciones desde el punto de vista del programador.

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):

PersistenceService service = new


PersistenceService(database);
Document doc = (Document) service.Find(typeof(Document),
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.

Una interfaz genérica para el acceso a bases de datos a través de


ADO.NET

En primer lugar, vamos a independizar la implementación de nuestro servicio de persistencia


de la base de datos concreta que podamos utilizar en un momento dado. Para lograrlo basta
con definir un interfaz genérico que implementaremos adecuadamente para nuestra base de
datos. Sólo tenemos que incluir cuatro métodos "factory" en este interfaz:

namespace DB
{
public interface Database
{
IDbConnection newConnection ();

IDbCommand newCommand (string sql, IDbConnection connection);

IDbDataAdapter newDataAdapter ();

IDbDataParameter newParameter (string name, DbType type);


}
}

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:

public class OleDbDatabase: Database


{
private string connectionString;

public OleDbDatabase(string connectionString)


{
this.connectionString = connectionString;
}

public IDbConnection newConnection ()


{
return new System.Data.OleDb.OleDbConnection(connectionString);
}

public IDbCommand newCommand (string sql, IDbConnection connection)


{
return new System.Data.OleDb.OleDbCommand
(sql, (System.Data.OleDb.OleDbConnection) connection);
}

public IDbDataAdapter newDataAdapter ()


{
return new System.Data.OleDb.OleDbDataAdapter();
}

public IDbDataParameter newParameter (string name,DbType type)


{
return new System.Data.OleDb.OleDbParameter(name,map(type) );
}

private System.Data.OleDb.OleDbType map (DbType type)


{
switch (type) {
case DbType.AnsiString: return
System.Data.OleDb.OleDbType.VarChar;
case DbType.String: return
System.Data.OleDb.OleDbType.VarChar;
case DbType.Date: return System.Data.OleDb.OleDbType.Date;
case DbType.Int32: return
System.Data.OleDb.OleDbType.Integer;
case DbType.Object: return
System.Data.OleDb.OleDbType.VarChar;
}
return System.Data.OleDb.OleDbType.Empty;
}
}

Construcción dinámica de consultas SQL

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:

public class SQLCommandBuilder


{
private static string ColumnList (DatabaseObjectModel model)
{
int i;
string sql = "";

if (model.Key.Size>0) {
sql += model.Key[0].ColumnName;

for (i=1; i<model.Key.Size; i++)


sql += ", "+model.Key[i].ColumnName;
}
if (model.Data.Size>0) {

if (model.Key.Size>0)
sql += ", ";

sql += model.Data[0].ColumnName;

for (i=1; i<model.Data.Size; i++)


sql += ", "+model.Data[i].ColumnName;
}

return sql;
}

private static string InsertList (DatabaseObjectModel model)


{
int i;
string sql = "?";

for (i=1; i<model.Key.Size+model.Data.Size; i++)


sql += ",?";

return sql;
}

private static string KeyWhereClause (DatabaseObjectModel model)


{
int i;
string where ="";

if (model.Key.Size>0) {
where = " WHERE "+model.Key[0].ColumnName+"=?";

for (i=1; i<model.Key.Size; i++)


where += " AND "+model.Key[i].ColumnName+"=?";
}

return where;
}

public static string SelectAll (DatabaseObjectModel model)


{
return "SELECT " + ColumnList(model) + " FROM " + model.Table;
}

public static string SelectKey (DatabaseObjectModel model)


{
return SelectAll(model) + KeyWhereClause(model);
}

public static string DeleteAll (DatabaseObjectModel model)


{
return "DELETE FROM " + model.Table;
}

public static string DeleteKey (DatabaseObjectModel model)


{
return DeleteAll(model) + KeyWhereClause(model);
}
public static string Insert (DatabaseObjectModel model)
{
return "INSERT INTO " +model.Table +" ("+ColumnList(model) + ")"
+ " VALUES ("+InsertList(model)+")";
}
}

La interpretación del código anterior es directa si conocemos la sintaxis del lenguaje de


consulta SQL [Structured Query Language].

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.

public class PersistenceService


{
private DB.Database database;
private ObjectFactory factory;

/// Constructor

public PersistenceService(DB.Database database)


{
this.database = database;
factory = new ObjectFactory(this);
}

/// Búsqueda de un objeto dada su clave

public object Find (Type type, object key)


{
return Find (type, new object[]{key} );
}

public object Find (Type type, object[] key)


{
DatabaseObjectModel model = new DatabaseObjectModel(type);
string sqlSelect = SQLCommandBuilder.SelectKey(model);
IDbConnection connection = database.newConnection();
IDbCommand selectCommand =
database.newCommand(sqlSelect,connection);
object[] data = null;
object obj = null;

if (model.Data.Size > 0) {

ParameterList(selectCommand,model.Key, key);

connection.Open();

try {

IDataReader reader = selectCommand.ExecuteReader();

if (reader.Read())
data = DataObjects(reader);

reader.Close();
reader = null;

} catch (Exception error) {


Console.Error.WriteLine(error);
}
selectCommand.Dispose();
selectCommand = null;
connection.Close();

if (data!=null)
obj = factory.Create(type,model,data);
}

return obj;
}

/// Borrado de un objeto

public void Delete (object obj)


{
DatabaseObjectModel model = new DatabaseObjectModel(obj.GetType());
string sqlDelete = SQLCommandBuilder.DeleteKey(model);
IDbConnection connection = database.newConnection();
IDbCommand deleteCommand =
database.newCommand(sqlDelete,connection);

ParameterList(deleteCommand,model.Key, obj);

connection.Open();

try {
deleteCommand.ExecuteNonQuery();
} catch (Exception error) {
Console.Error.WriteLine(error);
}

connection.Close();
}

/// Inserción de un objeto

public void Insert (object obj)


{
DatabaseObjectModel model = new DatabaseObjectModel(obj.GetType());
string sqlInsert = SQLCommandBuilder.Insert(model);
IDbConnection connection = database.newConnection();
IDbCommand insertCommand =
database.newCommand(sqlInsert,connection);

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

private object[] DataObjects (IDataReader reader)


{
int i;
object[] values = new object[reader.FieldCount];

for (i=0; i<reader.FieldCount; i++) {


if (reader.IsDBNull(i))
values[i] = null;
else
values[i] = reader.GetValue(i);
}

return values;
}

// Listas de parámetros

private void ParameterList


(IDbCommand command, DatabaseColumns columns, object obj)
{
int i;
IDbDataParameter param;
PropertyInfo property;
FieldInfo field;

for (i=0; i<columns.Size; i++) {


param = database.newParameter
("@"+columns[i].ColumnName, columns[i].ColumnType);
command.Parameters.Add (param);

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);
}
}
}

private void ParameterList


(IDbCommand command, DatabaseColumns columns, object[] key)
{
int i;
IDbDataParameter param;

for (i=0; i<columns.Size; i++) {


param = database.newParameter
("@"+columns[i].ColumnName, columns[i].ColumnType);
command.Parameters.Add (param);
param.Value = key[i];
}
}
}

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:

public class ObjectFactory


{
PersistenceService service;

public ObjectFactory (PersistenceService service)


{
this.service = service;
}

/// Creación dinámica de un objeto

public object Create (Type type, DatabaseObjectModel model, object[]


data)
{
Type[] types = new Type[0]; // Argumentos del constructor
object[] param = new object[0]; // Parámetros del constructor
ConstructorInfo constructor = type.GetConstructor
( BindingFlags.Instance | BindingFlags.Public,
null,
CallingConventions.HasThis, types, null );
object obj = null;

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;
}

/// Campos del objeto a partir de las columnas de la tabla

private void FillColumns ( object obj, Type type, DatabaseColumns


columns,
object[] data, int offset)
{
int i;
object value;

for (i=0; i<columns.Size; i++) {

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);
}

/// Claves externas

private void FillFK ( object obj, Type type, DatabaseObjectModel


model,
object[] data )
{
for (int i=0; i<model.getForeignKeyCount(); i++)
FillFK (obj,type,model,model.getForeignKey(i), data);
}

private void FillFK ( object obj, Type type, DatabaseObjectModel


model,
DatabaseColumns columns, object[] data)
{
int i;
object[] key = new object[columns.Size];
bool valid = true;

for (i=0; i<columns.Size; i++) {


key[i] = GetValue(model,columns[i],data);

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);
}
}

private static Type GetType (Type type, string name)


{
FieldInfo field = type.GetField(name);

if (field!=null){
return field.FieldType;
else
return null;}
}

private object GetValue


(DatabaseObjectModel model, DatabaseColumn column, object[]
data)
{
int pos = GetPosition(model,column);

if ( pos>=0 )
return data[pos];
else
return null;
}

private int GetPosition (DatabaseObjectModel model, DatabaseColumn


column)
{
int i;
int pos = -1;
for (i=0; (i<model.Key.Size) && (pos<0); i++)
if (model.Key.Columns[i] == column)
pos = i;

for (i=0; (i<model.Data.Size) && (pos<0); i++)


if (model.Data.Columns[i] == column)
pos = model.Key.Size + i;

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.

Implementación básica de O/R Mapping en la plataforma .NET


Tendencias

El principal objetivo de las empresas de desarrollo de software es maximizar su ROI [Return


On Investment], para lo cual no cabe duda que la mejor opción es maximizar la reutilización
siempre que utilizar algo hecho resulte más económico que rehacerlo. Es aquí donde se
enmarcan tres de las líneas de acción más prometedoras en la actualidad, las cuales fomentan
la reutilización desde distintos puntos de vista:

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/).

Posteriormente, surgió la programación orientada a objetos, en la cual los objetos encapsulan


tanto a los datos (miembros, campos, variables de instancia o propiedades, según el lenguaje)
como a las operaciones que se pueden efectuar sobre ellos (métodos, funciones y
procedimientos). Utilizar un objeto es muy fácil, sólo hay que crear una instancia de la clase
deseada e invocar los métodos definidos en su interfaz. Sin embargo, el diseño de nuevos
tipos de objetos es bastante difícil porque, a veces, entran en conflicto distintos objetivos
(simplicidad, funcionalidad, rendimiento, capacidad de reutilización, etc.).

Hoy en día, el énfasis recae en el desarrollo de componentes, módulos independientes que se


pueden reutilizar, los cuales suelen ser de una granularidad mayor que los objetos. Mientras
que los objetos son construcciones del lenguaje, los componentes, en principio, son
independientes del lenguaje. De hecho, el programador que utiliza un componente puede que
ni siquiera conozca al que lo desarrolló, ni trabaje para la misma empresa, ni utilice el mismo
lenguaje de programación.

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:

 Microsoft comercializa su plataforma .NET, cuyos orígenes se remontan a


COM [Component Object Model], estándar con el cual está preparada
para funcionar. De hecho, COM es la base de tecnologías como ActiveX u
OLE.
 Sun Microsystems promueve EJB [Enterprise Java Beans], un estándar
ligado a los servidores de aplicaciones J2EE [Java 2 Enterprise Edition] que
emplea el lenguaje de programación Java.
 El OMG [Object Management Group] propone su estándar CORBA [Common
Object-Request Broker], el cual proviene de su experiencia en el
desarrollo de aplicaciones empresariales en entornos distribuidos.
 Finalmente, los servicios web se pueden ver como una opción más en el
desarrollo de componentes.

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...

 Clemens Szyperski: Component Software: Beyond object-oriented


programming. Addison-Wesley, 2002 [2nd edition]. ISBN 0-201-74572-0

Creación de líneas de productos


Aparte del desarrollo de componentes, la creación de líneas de productos es otra línea
prometedora de acción para fomentar la reutilización. El desarrollo de líneas de productos
consiste en construir un sistema genérico [framework] a partir del cual se pueden derivar con
facilidad distintos productos (instanciaciones del framework). Mientras que el desarrollo de
componentes hace hincapié en el fomento de la reutilización de módulos, el desarrollo de
líneas de productos centra su atención en la reutilización de la infraestructura común a
distintos productos.

El desarrollo de frameworks requiere identificar aspectos comunes y puntos de variación en


familias de aplicaciones relacionadas. La instanciación de un framework, proceso clave que
posibilita la existencia de líneas de productos, consiste en rellenar los huecos dejados por el
armazón del framework. En otras palabras, el framework proporciona la infraestructura que la
aplicación ha de personalizar para responder a las necesidades de los usuarios.

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.

MDSD [Model-Driven Software Development]


Existen múltiples plataformas sobre las cuales podemos implementar nuestras aplicaciones.
Además, las tecnologías que forman parte de cada una de esas plataformas está en continua
evolución, lo que supone un gran gasto y muchísimo esfuerzo para las empresas de desarrollo
de software, que intentan sobrevivir de las aplicaciones que construyen y no de lo último en
tecnología, aunque a menudo tengan que adaptar sus sistemas para seguir la última moda.

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).

MDA, en concreto, propone crear un modelo inicial independiente de la tecnología [PIM:


Platform Independent Model] en el que se represente la funcionalidad de la aplicación. A partir
del PIM se generan diferentes modelos específicos para las tecnologías utilizadas [PSMs:
Platform Specific Models], así como los puentes que puedan necesitarse para que interactúen
entre ellos. Estos modelos son ya cercanos a la tecnología empleada (interfaces web, bases de
datos relacionales, clases C#...). Finalmente, a partir de los PSMs se genera el código de la
aplicación que implementa la funcionalidad especificada en el modelo abstracto de alto nivel.

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

También podría gustarte